148 lines
6.7 KiB
TypeScript
148 lines
6.7 KiB
TypeScript
import { BadRequestException, Body, Controller, Get, Post } from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { ProtocolLogsService, type ProtocolLogInput } from '../protocol-logs/protocol-logs.service';
|
|
import type {
|
|
GatewayReceiptEventDto,
|
|
GatewaySubmitDeadLetterDto,
|
|
GatewaySubmitResultDto,
|
|
GatewaySubmitSegmentResultDto,
|
|
GatewayUplinkEventDto,
|
|
} from './send-chain.contracts';
|
|
import { SendChainService } from './send-chain.service';
|
|
|
|
@Controller()
|
|
export class GatewayCallbackController {
|
|
constructor(
|
|
private readonly sendChain: SendChainService,
|
|
private readonly protocolLogs: ProtocolLogsService,
|
|
private readonly prisma: PrismaService,
|
|
) {}
|
|
|
|
@Get('health')
|
|
async health() {
|
|
await this.prisma.$queryRaw`SELECT 1`;
|
|
return { status: 'ok', role: 'gateway-callback', databasePool: this.prisma.getPoolState() };
|
|
}
|
|
|
|
@Post('gateway/events/submit-result')
|
|
submitResult(@Body() body: GatewaySubmitResultDto) {
|
|
return this.sendChain.handleSubmitResult(body);
|
|
}
|
|
|
|
@Post('gateway/events/submit-segment-result')
|
|
submitSegmentResult(@Body() body: GatewaySubmitSegmentResultDto) {
|
|
return this.sendChain.handleSubmitSegmentResult(body);
|
|
}
|
|
|
|
@Post('gateway/events/receipt/intake')
|
|
receiptIntake(@Body() body: GatewayReceiptEventDto) {
|
|
return this.track('deliver_receipt', body, () => this.sendChain.intakeReceipt(body));
|
|
}
|
|
|
|
@Post('gateway/events/receipt')
|
|
receipt(@Body() body: GatewayReceiptEventDto) {
|
|
return this.track('deliver_receipt', body, () => this.sendChain.handleReceipt(body));
|
|
}
|
|
|
|
@Post('gateway/events/uplink')
|
|
uplink(@Body() body: GatewayUplinkEventDto) {
|
|
return this.track('deliver_uplink', body, () => this.sendChain.handleUplink(body));
|
|
}
|
|
|
|
@Post('gateway/events/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'
|
|
);
|
|
if (body.protocol !== 'cmpp' || !allowedPacket || !['success', 'failed'].includes(body.status)) {
|
|
throw new BadRequestException('Unsupported Gateway callback protocol log event');
|
|
}
|
|
this.protocolLogs.record(body);
|
|
return { accepted: true };
|
|
}
|
|
|
|
@Post('gateway/events/dead-letter')
|
|
deadLetter(@Body() body: GatewaySubmitDeadLetterDto) {
|
|
return this.sendChain.recordGatewaySubmitDeadLetter(body);
|
|
}
|
|
|
|
@Post('gateway/events/batch')
|
|
async batch(@Body() body: {
|
|
batchId?: string;
|
|
gatewayInstanceId?: string;
|
|
createdAt?: string;
|
|
events?: Array<{ eventId?: string; type?: string; payload?: Record<string, unknown> }>;
|
|
}) {
|
|
if (!body.batchId || !body.gatewayInstanceId || !Array.isArray(body.events) || body.events.length < 1 || body.events.length > 100) {
|
|
throw new BadRequestException('batchId, gatewayInstanceId and 1 to 100 events are required');
|
|
}
|
|
if (Buffer.byteLength(JSON.stringify(body), 'utf8') > 1024 * 1024) {
|
|
throw new BadRequestException('Gateway callback batch exceeds 1MB');
|
|
}
|
|
const seen = new Set<string>();
|
|
const results: Array<{ eventId: string; accepted: boolean; retryable?: boolean; errorCode?: string }> = [];
|
|
for (let offset = 0; offset < body.events.length; offset += 25) {
|
|
results.push(...await Promise.all(body.events.slice(offset, offset + 25).map(async (event) => {
|
|
const eventId = String(event.eventId ?? '').trim();
|
|
if (!eventId || seen.has(eventId) || !event.payload || typeof event.payload !== 'object') {
|
|
return { eventId, accepted: false, retryable: false, errorCode: seen.has(eventId) ? 'DUPLICATE_EVENT_ID' : 'INVALID_EVENT' };
|
|
}
|
|
seen.add(eventId);
|
|
try {
|
|
await this.dispatchBatchEvent(String(event.type ?? ''), { ...event.payload, eventId });
|
|
return { eventId, accepted: true };
|
|
} catch (error) {
|
|
const invalid = error instanceof BadRequestException;
|
|
return { eventId, accepted: false, retryable: !invalid, errorCode: invalid ? 'INVALID_EVENT' : 'PROCESSING_FAILED' };
|
|
}
|
|
})));
|
|
}
|
|
return { batchId: body.batchId, results };
|
|
}
|
|
|
|
private dispatchBatchEvent(type: string, payload: Record<string, unknown>) {
|
|
switch (type) {
|
|
case 'submit_result': return this.sendChain.handleSubmitResult(payload as unknown as GatewaySubmitResultDto);
|
|
case 'submit_segment_result': return this.sendChain.handleSubmitSegmentResult(payload as unknown as GatewaySubmitSegmentResultDto);
|
|
case 'receipt_intake': return this.track('deliver_receipt', payload, () => this.sendChain.intakeReceipt(payload as unknown as GatewayReceiptEventDto));
|
|
case 'uplink': return this.track('deliver_uplink', payload, () => this.sendChain.handleUplink(payload as unknown as GatewayUplinkEventDto));
|
|
case 'dead_letter': return this.sendChain.recordGatewaySubmitDeadLetter(payload as unknown as GatewaySubmitDeadLetterDto);
|
|
default: throw new BadRequestException(`Unsupported batch event type ${type}`);
|
|
}
|
|
}
|
|
|
|
private async track<T>(eventType: string, body: object, action: () => Promise<T> | T) {
|
|
const startedAt = Date.now();
|
|
const value = body as Record<string, unknown>;
|
|
const common: Omit<ProtocolLogInput, 'status'> = {
|
|
protocol: 'cmpp', direction: 'channel_to_platform', eventType,
|
|
tenantId: value.tenantId as string, applicationId: value.applicationId as string,
|
|
channelId: value.channelId as string,
|
|
messageId: (value.messageId ?? value.platformMessageId) as string,
|
|
gatewayMessageId: (value.gatewayMessageId ?? value.msgId ?? value.upstreamMessageId) as string,
|
|
phone: (value.phoneNumber ?? value.srcTerminalId) as string,
|
|
resultCode: (value.rawStatus ?? value.status ?? value.stat) as string,
|
|
};
|
|
try {
|
|
const result = await action();
|
|
const resolved = result && typeof result === 'object' ? result as Record<string, unknown> : {};
|
|
if (process.env.PROTOCOL_LOG_CALLBACK_TRACKING_ENABLED === 'true') this.protocolLogs.record({
|
|
...common,
|
|
tenantId: (resolved.tenantId ?? common.tenantId) as string,
|
|
applicationId: (resolved.applicationId ?? common.applicationId) as string,
|
|
messageId: (resolved.messageId ?? common.messageId) as string,
|
|
status: 'success', durationMs: Date.now() - startedAt,
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
if (process.env.PROTOCOL_LOG_CALLBACK_TRACKING_ENABLED === 'true') this.protocolLogs.record({
|
|
...common, status: 'failed', durationMs: Date.now() - startedAt,
|
|
detail: { error: error instanceof Error ? error.message : String(error) },
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
}
|