import { BadRequestException, Body, Controller, Optional, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { GatewayInboundAuthDto, GatewayInboundSubmitDto, GatewayPendingDeliveryQueryDto, GatewayDownstreamAcknowledgedDto, GatewayDownstreamFailureType, GatewayDownstreamSentDto, GatewayDownstreamRecoveryStatusDto, GatewayReceiptEventDto, GatewaySubmitDeadLetterDto, GatewaySubmitResultDto, GatewaySubmitSegmentResultDto, GatewayUplinkEventDto, } from './send-chain.contracts'; import { SendChainService } from './send-chain.service'; import { GatewayDownstreamConnectionEventDto } from '../sms-config/sms-config.contracts'; import { SmsConfigService } from '../sms-config/sms-config.service'; import { ProtocolLogsService, type ProtocolLogInput } from '../protocol-logs/protocol-logs.service'; import { SecurityDetectionService } from '../security-detection/security-detection.service'; import { MetricsService } from '../metrics/metrics.service'; @ApiTags('gateway-events') @Controller('gateway/events') export class GatewayEventsController { constructor( private readonly sendChain: SendChainService, private readonly smsConfig: SmsConfigService, private readonly protocolLogs: ProtocolLogsService, private readonly security: SecurityDetectionService, @Optional() private readonly metrics?: MetricsService, ) {} @Post('submit-result') submitResult(@Body() body: GatewaySubmitResultDto) { return this.sendChain.handleSubmitResult(body); } @Post('submit-segment-result') submitSegmentResult(@Body() body: GatewaySubmitSegmentResultDto) { return this.sendChain.handleSubmitSegmentResult(body); } @Post('receipt/intake') receiptIntake(@Body() body: GatewayReceiptEventDto) { return this.trackGatewayEvent('deliver_receipt', body, () => this.sendChain.intakeReceipt(body)); } @Post('receipt') receipt(@Body() body: GatewayReceiptEventDto) { return this.trackGatewayEvent('deliver_receipt', body, () => this.sendChain.handleReceipt(body)); } @Post('uplink') uplink(@Body() body: GatewayUplinkEventDto) { return this.trackGatewayEvent('deliver_uplink', body, () => this.sendChain.handleUplink(body)); } @Post('protocol-log') protocolLog(@Body() body: ProtocolLogInput) { const allowedPacket = ( body.direction === 'platform_to_channel' && ['submit', 'deliver_resp'].includes(body.eventType) ) || ( body.direction === 'channel_to_platform' && body.eventType === 'submit_resp' ) || ( body.direction === 'platform_to_client' && ['submit_resp', 'deliver_receipt', 'deliver_uplink'].includes(body.eventType) ) || ( body.direction === 'client_to_platform' && body.eventType === 'deliver_resp' ); if (body.protocol !== 'cmpp' || !allowedPacket || !['success', 'failed'].includes(body.status)) { throw new BadRequestException('Unsupported Gateway protocol log event'); } this.protocolLogs.record(body); return { accepted: true }; } @Post('dead-letter') deadLetter(@Body() body: GatewaySubmitDeadLetterDto) { return this.sendChain.recordGatewaySubmitDeadLetter(body); } @Post('inbound/authenticate') async authenticateInbound(@Body() body: GatewayInboundAuthDto) { try { return await this.trackGatewayEvent('connect', body, () => this.sendChain.authenticateInboundApplication(body), 'client_to_platform'); } catch (error) { if (body.remoteIp) await this.security.recordEvent({ ruleCode: 'cmpp_auth_failure', sourceIp: body.remoteIp, account: body.account, protocol: body.version ?? 'cmpp', resultCode: error instanceof Error ? error.name : 'AUTH_FAILED' }).catch(() => undefined); throw error; } } @Post('inbound/submit') submitInbound(@Body() body: GatewayInboundSubmitDto) { return this.trackGatewayEvent('submit', body, () => this.sendChain.submitInboundMessage(body), 'client_to_platform'); } @Post('inbound/connection') inboundConnection(@Body() body: GatewayDownstreamConnectionEventDto) { return this.smsConfig.recordDownstreamConnectionEvent(body); } @Post('downstream/pending') pendingDownstream(@Body() body: GatewayPendingDeliveryQueryDto) { return this.sendChain.listPendingDownstreamDeliveries(body); } @Post('downstream/delivered') downstreamDelivered(@Body() body: { id: string }) { return this.sendChain.markDownstreamDeliveryDelivered(body.id); } @Post('downstream/sent') downstreamSent(@Body() body: GatewayDownstreamSentDto) { return this.sendChain.markDownstreamDeliverySent(body); } @Post('downstream/acknowledged') downstreamAcknowledged(@Body() body: GatewayDownstreamAcknowledgedDto) { return this.sendChain.acknowledgeDownstreamDelivery(body); } @Post('downstream/failed') downstreamFailed(@Body() body: GatewayDownstreamSentDto & { errorMessage?: string; failureType?: GatewayDownstreamFailureType }) { return this.sendChain.markDownstreamDeliveryFailed(body.id, body.errorMessage, body.failureType, body); } @Post('downstream/recovery-status') downstreamRecoveryStatus(@Body() body: GatewayDownstreamRecoveryStatusDto) { return this.sendChain.recordGatewayDownstreamRecoveryStatus(body); } private async trackGatewayEvent( eventType: string, body: object, action: () => Promise | T, direction: ProtocolLogInput['direction'] = 'channel_to_platform', ) { const startedAt = Date.now(); const inboundMetricStartedAt = eventType === 'submit' && direction === 'client_to_platform' ? this.metrics?.beginCmppInboundStage() : undefined; const value = body as Record; const common: Omit = { protocol: 'cmpp', direction, eventType, tenantId: value.tenantId as string, applicationId: value.applicationId as string, channelId: value.channelId as string, account: (value.account ?? value.loginAccount) as string, messageId: (value.messageId ?? value.platformMessageId) as string, gatewayMessageId: (value.gatewayMessageId ?? value.msgId ?? value.upstreamMessageId) as string, phone: (value.phoneNumber ?? value.srcTerminalId ?? value.destinationId) as string, resultCode: (value.result ?? value.submitStatus ?? value.rawStatus ?? value.status ?? value.stat) as string, detail: { sequenceId: value.sequenceId, connectionId: value.connectionId }, }; try { const result = await action(); const resultValue = result && typeof result === 'object' ? result as Record : {}; this.protocolLogs.record({ ...common, tenantId: (resultValue.tenantId ?? common.tenantId) as string, applicationId: (resultValue.applicationId ?? common.applicationId) as string, channelId: common.channelId ?? resultValue.channelId as string, account: common.account ?? resultValue.account as string, messageId: (resultValue.messageId ?? common.messageId) as string, gatewayMessageId: common.gatewayMessageId ?? (resultValue.gatewayMessageId ?? resultValue.msgId) as string, resultCode: common.resultCode ?? (resultValue.result ?? resultValue.status) as string, status: 'success', durationMs: Date.now() - startedAt, }); if (inboundMetricStartedAt != null) { this.metrics?.finishCmppInboundStage(inboundMetricStartedAt, 'total', 'success'); } return result; } catch (error) { this.protocolLogs.record({ ...common, status: 'failed', durationMs: Date.now() - startedAt, detail: { error: error instanceof Error ? error.message : 'unknown error' }, }); if (inboundMetricStartedAt != null) { this.metrics?.finishCmppInboundStage(inboundMetricStartedAt, 'total', 'error'); } throw error; } } }