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 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; 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; }; /** * 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, ) { this.batchEntry = new SendBatchEntryService(prisma, billing, riskReview, phoneFrequency, phoneRouting, facade, callbacks); this.inboundEntry = new SendInboundEntryService(prisma, billing, riskReview, phoneFrequency, phoneRouting, facade, callbacks); 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); } onModuleDestroy() { return this.gatewaySubmit.onModuleDestroy(); } 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 { 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) { return this.batchEntry.validateSendResources(tenantId, applicationId, templateId); } async reserveDailySendQuota(applicationId: string, requestedCount: number) { return this.batchEntry.reserveDailySendQuota(applicationId, requestedCount); } async tryReserveDailySendQuota(applicationId: string, requestedCount: number) { return this.batchEntry.tryReserveDailySendQuota(applicationId, requestedCount); } 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>, requestedGroupMessageId?: string, ) { return this.inboundEntry.submitCompleteInboundMessage(data, phoneNumbers, application, requestedGroupMessageId); } async collectInboundLongMessageFragment( data: GatewayInboundSubmitDto, application: NonNullable>>, 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, synchronousRejection?: { code: string; reason: string }, receiptRejection?: { code: string; reason: string }, ) { return this.inboundEntry.submitInboundSingleMessage(data, messageId, submitGroupMessageId, synchronousRejection, receiptRejection); } async evaluateRiskWithPhoneFrequency(input: { tenantId: string; applicationId: string; templateId?: string; content: string; variables?: Record; phoneNumber: string; sourceType: 'cmpp'; }) { return this.inboundEntry.evaluateRiskWithPhoneFrequency(input); } 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) { return this.gatewaySubmit.enqueueBatchTask(taskId); } startWorker() { return this.gatewaySubmit.startWorker(); } 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; }, 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[] } = {}, ): Promise { return this.gatewaySubmit.selectChannelForMessage(message, options); } async findApplicationRoute(tenantId: string, applicationId: string | undefined, carrier: string) { return this.gatewaySubmit.findApplicationRoute(tenantId, applicationId, carrier); } 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, ) { return this.gatewaySubmit.ensureSignatureReportedForChannel(message, channelId); } 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) { return this.gatewaySubmit.refreshTaskProgress(batchTaskId); } getSendQueue(): Queue { 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); } }