Files
lislgosms/api/src/send-chain/send-submission.service.ts
T
2026-09-16 18:27:29 +08:00

439 lines
14 KiB
TypeScript

import { BillingService } from '../billing/billing.service';
import { Queue } from 'bullmq';
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 type {
CreateBatchTaskDto,
CreateHttpBatchTaskDto,
GatewayInboundAuthDto,
GatewayInboundSubmitDto,
ImportPreviewDto,
ConfirmImportDto,
SendJob,
QueuePriority,
RoutedChannel,
} from './send-chain.contracts';
import { SendBatchEntryService } from './send-batch-entry.service';
import { SendGatewaySubmitService } from './send-gateway-submit.service';
import { SendInboundEntryService } from './send-inbound-entry.service';
import { SendReviewContinuationService } from './send-review-continuation.service';
import { SendScheduledDispatchService } from './send-scheduled-dispatch.service';
export type SendSubmissionCallbacks = {
releaseMessageReservation: (
message: {
tenantId: string;
batchTaskId: string;
messageId: string;
amountCents: number | bigint;
billingUnits: number;
},
remark: string,
) => Promise<void>;
recordCmppFailureReceipt: (
message: {
id: string;
tenantId?: string | null;
batchTaskId?: string | null;
applicationId?: string | null;
messageId: string;
phoneNumber: string;
cmppSubmitSequenceId?: string | null;
cmppSubmitGroupMessageId?: string | null;
cmppRegisteredDelivery?: boolean | null;
},
errorCode: string,
reason: string,
) => Promise<unknown>;
};
export type SendResourceValidationOptions = {
usePersistedTemplateSnapshot?: boolean;
httpRequest?: boolean;
};
/**
* R9 internal compatibility facade. SendChainService remains the only public NestJS provider.
*/
export class SendSubmissionService {
private readonly batchEntry: SendBatchEntryService;
private readonly inboundEntry: SendInboundEntryService;
private readonly reviewContinuation: SendReviewContinuationService;
private readonly scheduledDispatch: SendScheduledDispatchService;
private readonly gatewaySubmit: SendGatewaySubmitService;
constructor(
prisma: PrismaService,
billing: BillingService,
riskReview: RiskReviewService,
phoneFrequency: PhoneFrequencyService,
phoneRouting: PhoneRoutingLookupService,
facade: SendSubmissionService,
callbacks: SendSubmissionCallbacks,
metrics?: MetricsService,
) {
this.batchEntry = new SendBatchEntryService(
prisma,
billing,
riskReview,
phoneFrequency,
phoneRouting,
facade,
callbacks,
);
this.inboundEntry = new SendInboundEntryService(
prisma,
billing,
riskReview,
phoneFrequency,
phoneRouting,
facade,
callbacks,
metrics,
);
this.reviewContinuation = new SendReviewContinuationService(
prisma,
billing,
riskReview,
phoneFrequency,
phoneRouting,
facade,
callbacks,
);
this.scheduledDispatch = new SendScheduledDispatchService(
prisma,
billing,
riskReview,
phoneFrequency,
phoneRouting,
facade,
callbacks,
);
this.gatewaySubmit = new SendGatewaySubmitService(
prisma,
billing,
riskReview,
phoneFrequency,
phoneRouting,
facade,
callbacks,
metrics,
);
}
onModuleDestroy() {
return Promise.all([this.gatewaySubmit.onModuleDestroy(), this.inboundEntry.stopInboundWorkflowWorker()]);
}
async createBatchTask(data: CreateBatchTaskDto) {
return this.batchEntry.createBatchTask(data);
}
async createHttpBatchTask(data: CreateHttpBatchTaskDto) {
return this.batchEntry.createHttpBatchTask(data);
}
async getBatchTask(taskId: string, tenantId?: string, sourceType = 'client') {
return this.batchEntry.getBatchTask(taskId, tenantId, sourceType);
}
async previewImport(data: ImportPreviewDto) {
return this.batchEntry.previewImport(data);
}
async confirmImport(data: ConfirmImportDto) {
return this.batchEntry.confirmImport(data);
}
async resolveUnitPrice(tenantId: string, applicationId?: string) {
return this.batchEntry.resolveUnitPrice(tenantId, applicationId);
}
async resolveQueuePriority(tenantId: string, applicationId?: string): Promise<QueuePriority> {
return this.batchEntry.resolveQueuePriority(tenantId, applicationId);
}
async resolveApplicationAccessNumber(tenantId: string, applicationId?: string) {
return this.batchEntry.resolveApplicationAccessNumber(tenantId, applicationId);
}
async resolveTemplateMessageClassification(
tenantId: string,
applicationId: string | undefined,
templateId: string | undefined,
content: string,
) {
return this.batchEntry.resolveTemplateMessageClassification(tenantId, applicationId, templateId, content);
}
async classifyRejectedPhones(tenantId: string, applicationId: string | undefined, phones: string[]) {
return this.batchEntry.classifyRejectedPhones(tenantId, applicationId, phones);
}
async validateSendResources(
tenantId: string,
applicationId?: string,
templateId?: string,
options?: SendResourceValidationOptions,
) {
return this.batchEntry.validateSendResources(tenantId, applicationId, templateId, options);
}
async reserveDailySendQuota(applicationId: string, requestedCount: number) {
return this.batchEntry.reserveDailySendQuota(applicationId, requestedCount);
}
async tryReserveDailySendQuota(applicationId: string, requestedCount: number, reservationKey?: string) {
return this.batchEntry.tryReserveDailySendQuota(applicationId, requestedCount, reservationKey);
}
async authenticateInboundApplication(data: GatewayInboundAuthDto) {
return this.inboundEntry.authenticateInboundApplication(data);
}
async submitInboundMessage(data: GatewayInboundSubmitDto) {
return this.inboundEntry.submitInboundMessage(data);
}
async recoverCompletedInboundLongMessageResponse(messageId: string, phoneNumbers: string[]) {
return this.inboundEntry.recoverCompletedInboundLongMessageResponse(messageId, phoneNumbers);
}
async submitCompleteInboundMessage(
data: GatewayInboundSubmitDto,
phoneNumbers: string[],
application: Awaited<ReturnType<SendSubmissionService['findInboundApplication']>>,
requestedGroupMessageId?: string,
requestedMessageIds?: string[],
workflowKey?: string,
) {
return this.inboundEntry.submitCompleteInboundMessage(
data,
phoneNumbers,
application,
requestedGroupMessageId,
requestedMessageIds,
workflowKey,
);
}
async collectInboundLongMessageFragment(
data: GatewayInboundSubmitDto,
application: NonNullable<Awaited<ReturnType<SendSubmissionService['findInboundApplication']>>>,
phoneNumbers: string[],
) {
return this.inboundEntry.collectInboundLongMessageFragment(data, application, phoneNumbers);
}
async expireInboundLongMessages(now = new Date()) {
return this.inboundEntry.expireInboundLongMessages(now);
}
async submitInboundSingleMessage(
data: GatewayInboundSubmitDto & { phoneNumber: string },
messageId: string,
submitGroupMessageId: string,
application: NonNullable<Awaited<ReturnType<SendSubmissionService['findInboundApplication']>>>,
synchronousRejection?: { code: string; reason: string },
receiptRejection?: { code: string; reason: string },
workflowItemKey?: string,
) {
return this.inboundEntry.submitInboundSingleMessage(
data,
messageId,
submitGroupMessageId,
application,
synchronousRejection,
receiptRejection,
workflowItemKey,
);
}
async evaluateRiskWithPhoneFrequency(
input: {
tenantId: string;
applicationId: string;
templateId?: string;
content: string;
variables?: Record<string, unknown>;
phoneNumber: string;
sourceType: 'cmpp';
},
reservationKey?: string,
) {
return this.inboundEntry.evaluateRiskWithPhoneFrequency(input, reservationKey);
}
findInboundApplication(account: string) {
return this.inboundEntry.findInboundApplication(account);
}
async resolveInboundTemplateCandidate(applicationId: string, content: string) {
return this.inboundEntry.resolveInboundTemplateCandidate(applicationId, content);
}
resolveInboundSignatureCandidate(applicationId: string, content: string) {
return this.inboundEntry.resolveInboundSignatureCandidate(applicationId, content);
}
async resolveDrainageInfoMatch(signatureId: string | null | undefined, content: string) {
return this.inboundEntry.resolveDrainageInfoMatch(signatureId, content);
}
async attachMessageToReviewTask(
reviewTaskId: string,
messageRecordId: string,
signatureId: string,
drainageInfoId?: string,
) {
return this.inboundEntry.attachMessageToReviewTask(reviewTaskId, messageRecordId, signatureId, drainageInfoId);
}
async handleReviewDecision(reviewTaskId: string, decision: 'approved' | 'rejected', reason: string) {
return this.reviewContinuation.handleReviewDecision(reviewTaskId, decision, reason);
}
async dispatchDueScheduledTasks(now = new Date()) {
return this.scheduledDispatch.dispatchDueScheduledTasks(now);
}
async runScheduledDispatchScan() {
return this.scheduledDispatch.runScheduledDispatchScan();
}
async enqueueBatchTask(taskId: string, preparedMessage?: { messageRecordId: string; queuePriority?: string | null }) {
return this.gatewaySubmit.enqueueBatchTask(taskId, preparedMessage);
}
startWorker() {
return this.gatewaySubmit.startWorker();
}
startSubmitOutboxPublisher() {
return this.gatewaySubmit.startSubmitOutboxPublisher();
}
startInboundWorkflowWorker() {
return this.inboundEntry.startInboundWorkflowWorker();
}
stopInboundWorkflowWorker() {
return this.inboundEntry.stopInboundWorkflowWorker();
}
async processSendJob(job: SendJob) {
return this.gatewaySubmit.processSendJob(job);
}
async submitMessageToGateway(
message: {
id: string;
tenantId: string;
batchTaskId: string;
applicationId?: string | null;
templateId?: string | null;
signatureId?: string | null;
submitId?: string | null;
messageId: string;
phoneNumber: string;
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;
batchTask?: { sourceType?: string | null; phoneTotal?: number | null } | null;
},
routed: RoutedChannel,
attempt: number,
retryOfSubmitRecordId?: string,
) {
return this.gatewaySubmit.submitMessageToGateway(message, routed, attempt, retryOfSubmitRecordId);
}
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;
},
options: { forceNational?: boolean; excludeChannelIds?: string[]; previewOnly?: boolean } = {},
): Promise<RoutedChannel> {
return this.gatewaySubmit.selectChannelForMessage(message, options);
}
async findApplicationRoute(
tenantId: string,
applicationId: string | undefined,
carrier: string,
signatureId?: string,
) {
return this.gatewaySubmit.findApplicationRoute(tenantId, applicationId, carrier, signatureId);
}
async identifyCarrier(phoneNumber: string) {
return this.gatewaySubmit.identifyCarrier(phoneNumber);
}
async identifyProvince(phoneNumber: string) {
return this.gatewaySubmit.identifyProvince(phoneNumber);
}
async ensureSignatureReportedForChannel(
message: {
id: string;
templateId?: string | null;
template?: { signature?: { id?: string | null; name?: string | null } | null } | null;
signature?: { id?: string | null; name?: string | null } | null;
},
channelId: string,
carrier: string,
) {
return this.gatewaySubmit.ensureSignatureReportedForChannel(message, channelId, carrier);
}
async resolveMessageSignatureId(message: {
templateId?: string | null;
signatureId?: string | null;
template?: { signature?: { id?: string | null } | null } | null;
signature?: { id?: string | null } | null;
}) {
return this.gatewaySubmit.resolveMessageSignatureId(message);
}
async waitForChannelRateLimit(channelId: string, tps: number) {
return this.gatewaySubmit.waitForChannelRateLimit(channelId, tps);
}
async refreshTaskProgress(batchTaskId: string, knownSingleMessageStatus?: string) {
return this.gatewaySubmit.refreshTaskProgress(batchTaskId, knownSingleMessageStatus);
}
getSendQueue(): Queue<SendJob, unknown, 'send-message'> {
return this.gatewaySubmit.getSendQueue();
}
getGatewayQueue(): Queue {
return this.gatewaySubmit.getGatewayQueue();
}
getRedis() {
return this.gatewaySubmit.getRedis();
}
async publishGatewaySubmitCommand(command: unknown, idempotencyKey?: string) {
return this.gatewaySubmit.publishGatewaySubmitCommand(command, idempotencyKey);
}
}