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 trace query domain. Method bodies are preserved byte-for-byte from the facade baseline. export class OperationsTraceQueries { constructor(private readonly prisma: PrismaService) {} auditSummary(query: { tenantId?: string }) { return this.prisma.operationLog.groupBy({ by: ['action', 'resource'], where: { tenantId: query.tenantId }, _count: { _all: true }, orderBy: { _count: { action: 'desc' } }, take: 100, }); } async trace(query: TraceQuery) { const messages = await this.prisma.smsMessageRecord.findMany({ where: { ...messageWhere(query), messageId: query.messageId, }, include: { batchTask: { include: { apiRequests: true } }, submitRecords: { include: { session: true } }, receiptRecords: true, }, orderBy: { queuedAt: 'desc' }, take: 100, }); const messageIds = messages.map((message) => message.messageId); const [billingRecords, uplinks] = await Promise.all([ this.prisma.smsBillingRecord.findMany({ where: { tenantId: query.tenantId, taskId: query.taskId, messageId: messageIds.length > 0 ? { in: messageIds } : undefined, }, orderBy: { createdAt: 'desc' }, }), this.prisma.smsUplinkMessage.findMany({ where: { tenantId: query.tenantId, messageId: messageIds.length > 0 ? { in: messageIds } : undefined, }, orderBy: { receivedAt: 'desc' }, }), ]); return { messages, billingRecords, uplinks }; } async reconciliation(query: { tenantId?: string; taskId?: string }) { const [messages, billing, transactions] = await Promise.all([ this.prisma.smsMessageRecord.aggregate({ where: messageWhere({ tenantId: query.tenantId, taskId: query.taskId }), _count: { _all: true }, _sum: { amountCents: true, billingUnits: true }, }), this.prisma.smsBillingRecord.aggregate({ where: { tenantId: query.tenantId, taskId: query.taskId }, _count: { _all: true }, _sum: { amountCents: true, billingUnits: true }, }), this.prisma.accountTransaction.aggregate({ where: { tenantId: query.tenantId, relatedType: query.taskId ? { in: ['sms_batch_task', 'sms_message_record'] } : undefined, relatedId: query.taskId, }, _count: { _all: true }, _sum: { amountCents: true }, }), ]); const messageAmount = moneyToNumber(messages._sum.amountCents); const billingAmount = moneyToNumber(billing._sum.amountCents); const transactionAmount = moneyToNumber(transactions._sum.amountCents); return { messages, billing, transactions, diff: { messageVsBillingAmountCents: messageAmount - billingAmount, billingVsTransactionAmountCents: billingAmount + transactionAmount, messageVsBillingUnits: (messages._sum.billingUnits ?? 0) - (billing._sum.billingUnits ?? 0), }, }; } }