import { BadRequestException, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { randomUUID } from 'node:crypto'; import { moneyToNumber } from '../../common/money'; import { PrismaService } from '../../prisma/prisma.service'; import type { MessageQuery, TraceQuery, OperationLogQuery, GatewaySubmitDeadLetterQuery, DownstreamDeliveryQuery, DownstreamDeliveryDashboardQuery, DownstreamRecoveryStatusQuery, MessageSegmentAuditQuery, SignatureQualityQuery } from '../operations.contracts'; import { messageWhere, recognizedCarrierValues, carrierWhere, startOfShanghaiDay, endOfShanghaiDay, qualityBusinessDay, shanghaiDateKey, normalizeGroupBy, returnedTransactionWhere, createdAtRange, downstreamAlertPendingMinutes, downstreamAlertRecentFailedHours, downstreamAlertWindows, downstreamAlertWhere, stalledPendingWhere, downstreamDeliveryScopedWhere, parseDateBoundary, downstreamRecoveryStatusWhere, escapeCsvCell, formatCsvDate, formatExportTimestamp, clientApplicationView, clientReceiptView, clientMessageView, clientBatchTaskView, clientUplinkView, clientAccountView, clientRechargeView, summarizeMessageGroups, groupDownstreamByType, groupDownstreamByApplication, positiveInteger, operationLogLevelWhere, normalizeOperationLog, sanitizeGatewaySubmitException, redactGatewayCommandValue } from '../operations.helpers'; // R2 uplink query domain. Method bodies are preserved byte-for-byte from the facade baseline. export class OperationsUplinkQueries { constructor(private readonly prisma: PrismaService) {} listUplinkMessages(query: { tenantId?: string; channelId?: string; applicationId?: string; phoneNumber?: string; keyword?: string; startTime?: string; endTime?: string; page?: number; pageSize?: number }) { return this.prisma.smsUplinkMessage.findMany({ where: { tenantId: query.tenantId, channelId: query.channelId, applicationId: query.applicationId, phoneNumber: query.phoneNumber ? { contains: query.phoneNumber } : undefined, content: query.keyword ? { contains: query.keyword } : undefined, receivedAt: query.startTime || query.endTime ? { gte: query.startTime ? new Date(query.startTime) : undefined, lte: query.endTime ? new Date(query.endTime) : undefined } : undefined, }, include: { tenant: true, application: true, channel: true, messageRecord: { include: { application: true } }, matchCandidates: { include: { tenant: true, application: true, messageRecord: { include: { application: true, tenant: true, channel: true } }, }, orderBy: [{ status: 'asc' }, { confidence: 'desc' }, { createdAt: 'asc' }], }, }, orderBy: { receivedAt: 'desc' }, skip: query.page && query.pageSize ? (query.page - 1) * query.pageSize : undefined, take: query.pageSize ?? 500, }); } async listClientUplinkMessages(query: { tenantId?: string; applicationId?: string; phoneNumber?: string; keyword?: string; startTime?: string; endTime?: string; page?: number; pageSize?: number }) { const items = await this.listUplinkMessages(query); return items.map(clientUplinkView); } async listUplinkMessagesPage(query: { tenantId?: string; channelId?: string; applicationId?: string; phoneNumber?: string; keyword?: string; startTime?: string; endTime?: string; page?: number; pageSize?: number }, clientView = false) { const page = Math.max(1, Math.floor(Number(query.page) || 1)); const pageSize = Math.min(100, Math.max(1, Math.floor(Number(query.pageSize) || 10))); const where: Prisma.SmsUplinkMessageWhereInput = { tenantId: query.tenantId, channelId: query.channelId, applicationId: query.applicationId, phoneNumber: query.phoneNumber ? { contains: query.phoneNumber } : undefined, content: query.keyword ? { contains: query.keyword } : undefined, receivedAt: query.startTime || query.endTime ? { gte: query.startTime ? new Date(query.startTime) : undefined, lte: query.endTime ? new Date(query.endTime) : undefined, } : undefined, }; const [rawItems, total] = await Promise.all([ this.listUplinkMessages({ ...query, page, pageSize }), this.prisma.smsUplinkMessage.count({ where }), ]); return { items: clientView ? rawItems.map(clientUplinkView) : rawItems, total, page, pageSize, }; } async monitor(query: { tenantId?: string; channelId?: string }) { const where = messageWhere(query); const [byStatus, recentMessages, recentReceipts, recentUplinks] = await Promise.all([ this.prisma.smsMessageRecord.groupBy({ by: ['status'], where, _count: { _all: true } }), this.prisma.smsMessageRecord.findMany({ where, include: { submitRecords: true, receiptRecords: true }, orderBy: { queuedAt: 'desc' }, take: 20, }), this.prisma.smsReceiptRecord.findMany({ where: { tenantId: query.tenantId, channelId: query.channelId }, orderBy: { createdAt: 'desc' }, take: 20, }), this.listUplinkMessages({ tenantId: query.tenantId, channelId: query.channelId }), ]); return { byStatus, recentMessages, recentReceipts, recentUplinks: recentUplinks.slice(0, 20), }; } }