import { OperationsService } from './operations.service'; function createPrismaMock() { return { $queryRaw: jest.fn().mockResolvedValue([]), user: { findFirst: jest.fn().mockResolvedValue({ tenantId: 'tenant-1' }), }, tenant: { findUnique: jest.fn().mockResolvedValue({ id: 'tenant-1', name: '企业A' }), }, smsBatchTask: { findMany: jest.fn().mockResolvedValue([{ id: 'task-1', taskNo: 'BATCH-1' }]), count: jest.fn().mockResolvedValue(3), }, smsSendTask: { count: jest.fn().mockResolvedValue(2), }, smsMessageRecord: { findMany: jest.fn().mockResolvedValue([{ messageId: 'MSG-1' }]), count: jest.fn().mockResolvedValue(51), groupBy: jest.fn().mockResolvedValue([{ status: 'delivered', _count: { _all: 2 }, _sum: { amountCents: 20, billingUnits: 2 } }]), aggregate: jest.fn().mockResolvedValue({ _count: { _all: 2 }, _sum: { amountCents: 20, billingUnits: 2 } }), }, smsReceiptRecord: { findMany: jest.fn().mockResolvedValue([]), }, smsUplinkMessage: { findMany: jest.fn().mockResolvedValue([{ id: 'uplink-1', messageId: 'MSG-1' }]), count: jest.fn().mockResolvedValue(1), }, smsBillingRecord: { findMany: jest.fn().mockResolvedValue([{ id: 'bill-1', messageId: 'MSG-1' }]), aggregate: jest.fn().mockResolvedValue({ _count: { _all: 2 }, _sum: { amountCents: 20, billingUnits: 2 } }), }, accountTransaction: { aggregate: jest.fn().mockResolvedValue({ _count: { _all: 2 }, _sum: { amountCents: -20 } }), }, tenantAccount: { findMany: jest.fn().mockResolvedValue([{ tenantId: 'tenant-1', balanceCents: 1000, tenant: { name: '租户A' } }]), }, rechargeOrder: { findMany: jest.fn().mockResolvedValue([{ id: 'order-1', tenantId: 'tenant-1', amountCents: 1000 }]), }, smsTemplate: { count: jest.fn().mockResolvedValue(1), }, smsSignature: { count: jest.fn().mockResolvedValue(1), }, smsDrainageInfo: { count: jest.fn().mockResolvedValue(0), }, enterpriseCertification: { count: jest.fn().mockResolvedValue(1), findFirst: jest.fn().mockResolvedValue({ id: 'certification-1' }), }, smsApplication: { findMany: jest.fn().mockResolvedValue([ { id: 'app-1', name: '应用A' }, { id: 'app-2', name: '应用B' }, ]), }, cmppConnectionState: { groupBy: jest.fn().mockResolvedValue([{ status: 'connected', _count: { _all: 1 }, _sum: { currentConnections: 2, desiredConnections: 2 } }]), }, operationLog: { findMany: jest.fn().mockResolvedValue([{ id: 'log-1', tenantId: 'tenant-1', tenant: { name: '租户A' }, user: { displayName: '运营' }, action: 'billing.manual_recharge', resource: 'recharge_order', resourceId: 'order-1', detail: { amountCents: 1000 }, createdAt: new Date('2026-07-02T01:00:00.000Z'), }]), count: jest.fn().mockResolvedValue(1), groupBy: jest.fn().mockResolvedValue([{ resource: 'recharge_order', _count: { _all: 1 } }]), }, gatewaySubmitDeadLetter: { findMany: jest.fn().mockResolvedValue([{ id: 'dead-1', streamMessageId: '1710000000000-0', status: 'pending', failureCode: 'SUBMIT_PROCESSING_FAILED', failureMessage: 'network down', rawPayload: '{"upstream":{"passwordCipher":"secret"}}', commandPayload: { upstream: { account: 'sp', passwordCipher: 'secret' } }, tenant: { name: '租户A' }, application: { name: '应用A' }, channel: { code: 'CMPP-A' }, }]), count: jest.fn().mockResolvedValue(1), groupBy: jest.fn().mockResolvedValue([{ status: 'pending', _count: { _all: 1 } }]), findFirst: jest.fn().mockResolvedValue({ createdAt: new Date('2026-07-08T12:00:00.000Z') }), }, smsReceiptAnomaly: { findMany: jest.fn().mockResolvedValue([{ id: 'receipt-anomaly-1', anomalyKey: 'aggregate-receipt-conflict:record-1:SUB-1', anomalyType: 'aggregate_success_then_failure', status: 'pending', occurrenceCount: 1, firstOccurredAt: new Date('2026-08-06T01:00:00.000Z'), lastOccurredAt: new Date('2026-08-06T01:00:00.000Z'), tenant: { name: '租户A' }, application: { name: '应用A' }, channel: { name: '通道A' }, messageRecord: { messageId: 'MSG-1', phoneNumber: '13800000001', status: 'delivered' }, submitRecord: { submitId: 'SUB-1', submitStatus: 'accepted' }, receiptRecord: { gatewayMessageId: 'GW-1', receiptStatus: 'undelivered', rawStatus: 'UNDELIV' }, }]), count: jest.fn().mockResolvedValue(1), groupBy: jest.fn().mockResolvedValue([{ status: 'pending', _count: { _all: 1 } }]), findFirst: jest.fn().mockResolvedValue({ firstOccurredAt: new Date('2026-08-06T01:00:00.000Z') }), }, gatewayDownstreamRecoveryStatus: { findMany: jest.fn().mockResolvedValue([{ id: 'recover-1', account: '100001', state: 'waiting_connection', lockOwner: 'gateway-a', lockExpiresAt: new Date('2026-07-08T12:00:30.000Z'), attemptCount: 2, failureCategory: 'client_disconnected', nextRetryAt: new Date('2026-07-08T12:10:00.000Z'), lastError: 'downstream client is not connected', tenant: { name: '租户A' }, application: { name: '应用A' }, }]), findUnique: jest.fn().mockResolvedValue({ id: 'recover-1', account: '100001', state: 'waiting_connection', lockOwner: 'gateway-a', lockExpiresAt: new Date('2026-07-08T12:00:30.000Z'), attemptCount: 2, failureCategory: 'client_disconnected', nextRetryAt: new Date('2026-07-08T12:10:00.000Z'), lastAttemptAt: new Date('2026-07-08T12:00:00.000Z'), lastSuccessAt: null, lastFailureAt: new Date('2026-07-08T11:58:00.000Z'), lastError: 'downstream client is not connected', lastSkipReason: 'waiting for reconnect', gatewayInstanceId: 'gw-01', createdAt: new Date('2026-07-08T11:50:00.000Z'), updatedAt: new Date('2026-07-08T12:00:00.000Z'), tenant: { name: '租户A' }, application: { name: '应用A' }, }), count: jest.fn().mockResolvedValue(1), groupBy: jest.fn().mockResolvedValue([ { failureCategory: 'client_disconnected', _count: { _all: 1 } }, ]), }, smsMessageSegmentAudit: { findMany: jest.fn().mockResolvedValue([{ id: 'segment-1', messageRecordId: 'record-1', submitId: 'SUB-1', segmentTotal: 2, segmentIndex: 1, sequenceId: 7, gatewayMessageId: 'GW-1-A', submitStatus: 'accepted', receiptStatus: 'delivered', channel: { name: '通道A' }, }]), }, cmppDownstreamDelivery: { findMany: jest.fn().mockResolvedValue([{ id: 'delivery-1', tenantId: 'tenant-1', applicationId: 'app-1', messageId: 'MSG-1', deliveryType: 'receipt', status: 'failed', payload: { account: '100001', phoneNumber: '13800000001' }, retryCount: 10, lastError: 'client offline', tenant: { name: '租户A' }, application: { name: '应用A' }, messageRecord: { messageId: 'MSG-1' }, }]), count: jest.fn().mockResolvedValue(1), groupBy: jest.fn().mockResolvedValue([ { deliveryType: 'receipt', status: 'pending', _count: { _all: 2 } }, { deliveryType: 'receipt', status: 'failed', _count: { _all: 1 } }, { deliveryType: 'receipt', status: 'delivered', _count: { _all: 6 } }, { deliveryType: 'uplink', status: 'pending', _count: { _all: 1 } }, { deliveryType: 'uplink', status: 'delivered', _count: { _all: 2 } }, { applicationId: 'app-1', status: 'pending', _count: { _all: 2 } }, { applicationId: 'app-1', status: 'failed', _count: { _all: 1 } }, { applicationId: 'app-1', status: 'delivered', _count: { _all: 5 } }, { applicationId: 'app-2', status: 'pending', _count: { _all: 1 } }, { applicationId: 'app-2', status: 'delivered', _count: { _all: 3 } }, ]), }, }; } describe('OperationsService', () => { it('keeps task progress limited to client-created batch tasks', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await service.listBatchTasks({ tenantId: 'tenant-1', status: 'queued' }); expect(prisma.smsBatchTask.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: { tenantId: 'tenant-1', status: 'queued', sourceType: 'client' }, })); }); it('filters send-chain messages by tenant, application, channel, carrier, content, date, task, phone, and status', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await service.listMessages({ tenantId: 'tenant-1', applicationId: 'app-1', channelId: 'channel-1', channelKeyword: '移动通道', taskId: 'task-1', messageId: 'MSG-1', phoneNumber: '13800000001', contentKeyword: '验证码', carrier: 'mobile', queuedAtFrom: '2026-07-01', queuedAtTo: '2026-07-02', status: 'delivered', hasDrainage: 'true', }); expect(prisma.smsMessageRecord.findMany).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', applicationId: 'app-1', channelId: 'channel-1', batchTaskId: 'task-1', messageId: 'MSG-1', phoneNumber: '13800000001', carrier: { in: ['mobile', 'cmcc', '移动', '中国移动'] }, status: 'delivered', hasDrainageContent: true, content: { contains: '验证码', mode: 'insensitive' }, channel: { name: { contains: '移动通道', mode: 'insensitive' } }, queuedAt: { gte: new Date('2026-07-01T00:00:00+08:00'), lte: new Date('2026-07-02T23:59:59.999+08:00'), }, }, include: { tenant: true, application: true, channel: true, submitRecords: { include: { channel: true, channelGroup: true } }, receiptRecords: { include: { channel: true } }, downstreamDeliveries: { where: { deliveryType: 'receipt' }, select: { id: true, deliveryType: true, status: true, deliveredAt: true, lastError: true }, }, }, orderBy: { queuedAt: 'desc' }, }); }); it('treats null and nonstandard carrier values as unrecognized', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await service.listMessages({ carrier: 'unknown' }); expect(prisma.smsMessageRecord.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ AND: [ { OR: [ { carrier: null }, { carrier: { notIn: [ 'mobile', 'cmcc', '移动', '中国移动', 'unicom', 'cucc', '联通', '中国联通', 'telecom', 'ctcc', '电信', '中国电信', ], }, }, ], }, ], }), })); }); it('separates upstream submit failures from post-acceptance delivery failures', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await service.listMessages({ status: 'submit_failed' }); expect(prisma.smsMessageRecord.findMany).toHaveBeenLastCalledWith(expect.objectContaining({ where: expect.objectContaining({ OR: [{ status: 'submit_failed' }, { submitStatus: { in: ['rejected', 'timeout'] } }], }), })); await service.listMessages({ status: 'failed' }); expect(prisma.smsMessageRecord.findMany).toHaveBeenLastCalledWith(expect.objectContaining({ where: expect.objectContaining({ status: 'failed', submitStatus: 'accepted' }), })); }); it('paginates message records in PostgreSQL and limits heavy relations to the requested page', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.listMessagesPage({ tenantId: 'tenant-1', page: 2, pageSize: 25 })).resolves.toEqual({ items: [{ messageId: 'MSG-1' }], total: 51, page: 2, pageSize: 25, }); expect(prisma.smsMessageRecord.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ tenantId: 'tenant-1' }), skip: 25, take: 25, orderBy: [{ queuedAt: 'desc' }, { id: 'desc' }], })); expect(prisma.smsMessageRecord.count).toHaveBeenCalledWith({ where: expect.objectContaining({ tenantId: 'tenant-1' }), }); }); it('returns uplink messages with tenant and channel display data', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await service.listUplinkMessages({ tenantId: 'tenant-1', channelId: 'channel-1' }); expect(prisma.smsUplinkMessage.findMany).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', channelId: 'channel-1', applicationId: undefined, phoneNumber: undefined, content: undefined, receivedAt: 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' }, take: 500, }); }); it('returns client message views without supplier channel, submit, tenant, or gateway internals', async () => { const prisma = createPrismaMock(); prisma.smsMessageRecord.findMany.mockResolvedValue([{ id: 'record-1', tenantId: 'tenant-1', batchTaskId: 'task-1', applicationId: 'app-1', channelId: 'channel-1', messageId: 'MSG-1', phoneNumber: '13800000001', carrier: 'mobile', province: '上海', content: '验证码1234', billingUnits: 1, amountCents: 352, status: 'delivered', queuedAt: new Date('2026-07-21T01:00:00.000Z'), application: { id: 'app-1', name: '应用A', secretHash: 'secret' }, tenant: { id: 'tenant-1', name: '企业A' }, channel: { id: 'channel-1', account: 'supplier', passwordCipher: 'cipher', unitPrice: 200 }, submitRecords: [{ id: 'submit-1', gatewayMessageId: 'GW-1', channel: { passwordCipher: 'cipher' } }], receiptRecords: [{ id: 'receipt-1', messageId: 'MSG-1', gatewayMessageId: 'GW-1', receiptStatus: 'delivered', rawStatus: 'DELIVRD', errorCode: null, errorMessage: null, deliveredAt: new Date('2026-07-21T01:00:05.000Z'), createdAt: new Date('2026-07-21T01:00:05.000Z'), }], }]); const service = new OperationsService(prisma as never); const [message] = await service.listClientMessages({ tenantId: 'tenant-1' }); expect(message).toMatchObject({ id: 'record-1', messageId: 'MSG-1', carrier: 'mobile', province: '上海', application: { id: 'app-1', name: '应用A' }, receiptRecords: [expect.objectContaining({ receiptStatus: 'delivered', rawStatus: 'DELIVRD' })], }); expect(message).not.toHaveProperty('tenant'); expect(message).not.toHaveProperty('tenantId'); expect(message).not.toHaveProperty('channel'); expect(message).not.toHaveProperty('channelId'); expect(message).not.toHaveProperty('submitRecords'); expect(JSON.stringify(message)).not.toMatch(/passwordCipher|gatewayMessageId|unitPrice|supplier|cipher/); }); it('returns a client dashboard without gateway state or supplier channel secrets', async () => { const prisma = createPrismaMock(); prisma.smsBatchTask.findMany.mockResolvedValue([{ id: 'task-1', taskNo: 'BATCH-1', tenantId: 'tenant-1', applicationId: 'app-1', phoneTotal: 1, status: 'finished', createdAt: new Date('2026-07-21T01:00:00.000Z'), application: { id: 'app-1', name: '应用A' }, messages: [{ channel: { account: 'supplier', passwordCipher: 'cipher', unitPrice: 200 } }], }]); const service = new OperationsService(prisma as never); const dashboard = await service.clientDashboard({ tenantId: 'tenant-1' }); expect(dashboard.gatewayConnections).toEqual([]); expect(dashboard.recentTasks).toEqual([expect.objectContaining({ id: 'task-1', taskNo: 'BATCH-1' })]); expect(dashboard.clientOverview).toEqual({ enterpriseName: '企业A', certificationStatus: 'certified', signatureCount: 1, pendingBatchTaskCount: 3, }); expect(prisma.smsBatchTask.count).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', sourceType: 'client', status: 'pending_review' }, }); expect(prisma.enterpriseCertification.findFirst).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', status: 'approved' }, select: { id: true }, }); expect(prisma.smsSignature.count).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', auditStatus: { notIn: ['deleted', 'disabled'] } }, }); expect(JSON.stringify(dashboard)).not.toMatch(/passwordCipher|supplier|cipher|unitPrice/); }); it('builds dashboard and statistics aggregates', async () => { const prisma = createPrismaMock(); prisma.$queryRaw.mockResolvedValueOnce([{ tenantId: 'tenant-1', tenantName: '租户A', todaySpendCents: 24000n, balanceCents: 1000000n, creditCents: 50000n, }]).mockResolvedValueOnce([ { hour: 9, submittedCount: 12n, successCount: 10n }, { hour: 10, submittedCount: 5n, successCount: 4n }, ]).mockResolvedValueOnce([ { category: 'templates', count: 3n, averageProcessingMs: 90_000n }, { category: 'signatures', count: 2n, averageProcessingMs: 120_000n }, ]); prisma.cmppDownstreamDelivery.count = jest.fn() .mockResolvedValueOnce(3) .mockResolvedValueOnce(2) .mockResolvedValueOnce(8) .mockResolvedValueOnce(1) .mockResolvedValueOnce(1) .mockResolvedValueOnce(2); prisma.accountTransaction.aggregate.mockResolvedValueOnce({ _count: { _all: 2 }, _sum: { amountCents: 10 } }); const service = new OperationsService(prisma as never); await expect(service.dashboard({ tenantId: 'tenant-1' })).resolves.toEqual( expect.objectContaining({ taskCount: 3, uplinkCount: 1, pendingAuditCount: 5, pendingAudits: { enterpriseCertifications: 1, smsAudits: 2, signatures: 1, drainageInfos: 0, templates: 1, total: 5, }, today: expect.objectContaining({ returnedCents: 10 }), hourlySendTrend: expect.arrayContaining([ { hour: 9, label: '09:00', submittedCount: 12, successCount: 10 }, { hour: 10, label: '10:00', submittedCount: 5, successCount: 4 }, ]), auditProcessingSpeed: [ { category: 'enterpriseCertifications', label: '企业认证', count: 0, averageProcessingMs: null }, { category: 'smsAudits', label: '短信审核', count: 0, averageProcessingMs: null }, { category: 'templates', label: '模板', count: 3, averageProcessingMs: 90000 }, { category: 'signatures', label: '签名', count: 2, averageProcessingMs: 120000 }, { category: 'drainageInfos', label: '引流信息', count: 0, averageProcessingMs: null }, ], enterpriseSpendRanks: [{ tenantId: 'tenant-1', tenantName: '租户A', todaySpendCents: 24000, balanceCents: 1000000, creditCents: 50000, }], gatewayConnections: [{ status: 'connected', _count: { _all: 1 }, _sum: { currentConnections: 2, desiredConnections: 2 } }], downstreamDeliverySummary: expect.objectContaining({ pending: 3, failed: 2, delivered: 8, stalledPending: 1, stalledAck: 1, recentFailed: 2, alertCount: 4, }), }), ); expect(prisma.cmppDownstreamDelivery.count).toHaveBeenNthCalledWith(4, { where: { tenantId: 'tenant-1', status: 'pending', OR: [ { lastRetriedAt: null, createdAt: { lte: expect.any(Date) } }, { lastRetriedAt: { lte: expect.any(Date) } }, ], }, }); expect(prisma.cmppDownstreamDelivery.count).toHaveBeenNthCalledWith(5, { where: { tenantId: 'tenant-1', status: 'awaiting_ack', ackDeadlineAt: { lte: expect.any(Date) } }, }); expect(prisma.cmppDownstreamDelivery.count).toHaveBeenNthCalledWith(6, { where: { tenantId: 'tenant-1', status: { in: ['failed', 'unconfirmed', 'rejected'] }, updatedAt: { gte: expect.any(Date) }, }, }); const hourlyTrendQuery = prisma.$queryRaw.mock.calls[1]?.[0] as { sql?: string }; expect(hourlyTrendQuery.sql).toContain( `HOUR FROM (message."queuedAt" AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Shanghai'`, ); expect(prisma.accountTransaction.aggregate).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', createdAt: { gte: expect.any(Date) }, OR: [ { transactionType: 'refunded' }, { transactionType: 'released', relatedType: 'sms_message_record' }, ], }, _sum: { amountCents: true }, _count: { _all: true }, }); await service.statistics({ tenantId: 'tenant-1', groupBy: 'application' }); expect(prisma.smsMessageRecord.groupBy).toHaveBeenCalledWith({ by: ['applicationId'], where: expect.objectContaining({ tenantId: 'tenant-1' }), _count: { _all: true }, _sum: { amountCents: true, billingUnits: true }, }); }); it('returns real daily channel and signature quality for the selected Shanghai date', async () => { const prisma = createPrismaMock(); prisma.$queryRaw .mockResolvedValueOnce([{ channelId: 'channel-1', channelName: '通道一', total: 5, acceptedCount: 4, submitFailureCount: 1, submitFailureRate: 20, successCount: 3, unknownCount: 1, failureCount: 1, successRate: 60, unknownRate: 20, failureRate: 20, averageArrivalMs: 1200, }]) .mockResolvedValueOnce([{ id: 'signature-1:plain', signatureId: 'signature-1', signatureName: '【测试签名】', tenantId: 'tenant-1', tenantName: '租户A', hasDrainage: false, total: 5, acceptedCount: 4, submitFailureCount: 1, successCount: 3, unknownCount: 1, failureCount: 1, successRate: 60, averageArrivalMs: 1200, }]) .mockResolvedValueOnce([{ total: 5, successCount: 3, unknownCount: 1, failureCount: 1, successRate: 60, }]) .mockResolvedValueOnce([{ applicationId: 'app-1', applicationName: '通知应用', tenantId: 'tenant-1', tenantName: '租户A', total: 5, successCount: 3, unknownCount: 1, failureCount: 1, successRate: 60, }]); const service = new OperationsService(prisma as never); await expect(service.sendQuality('2026-07-24')).resolves.toEqual({ date: '2026-07-24', drainageSignatures: [], summary: { total: 5, successCount: 3, unknownCount: 1, failureCount: 1, successRate: 60, }, channels: [expect.objectContaining({ channelId: 'channel-1', total: 5, acceptedCount: 4, submitFailureCount: 1, submitFailureRate: 20, successRate: 60, })], signatures: [expect.objectContaining({ signatureId: 'signature-1', signatureName: '【测试签名】', hasDrainage: false, acceptedCount: 4, submitFailureCount: 1, })], applications: [expect.objectContaining({ applicationId: 'app-1', applicationName: '通知应用', tenantName: '租户A', total: 5 })], }); expect(prisma.$queryRaw).toHaveBeenCalledTimes(4); }); it('returns pending audit counts without running the full dashboard aggregation', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.pendingAudits('tenant-1')).resolves.toEqual({ enterpriseCertifications: 1, smsAudits: 2, templates: 1, signatures: 1, drainageInfos: 0, total: 5, }); expect(prisma.smsTemplate.count).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', auditStatus: 'pending' } }); expect(prisma.smsSignature.count).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', auditStatus: 'pending' } }); expect(prisma.smsDrainageInfo.count).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', auditStatus: 'pending' } }); expect(prisma.enterpriseCertification.count).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', status: 'pending' } }); expect(prisma.smsSendTask.count).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', status: 'pending_review' } }); expect(prisma.smsMessageRecord.groupBy).not.toHaveBeenCalled(); expect(prisma.$queryRaw).not.toHaveBeenCalled(); }); it('rejects invalid send quality dates', async () => { const service = new OperationsService(createPrismaMock() as never); await expect(service.sendQuality('2026-02-31')).rejects.toThrow('统计日期无效'); }); it('returns paged registered-signature quality with channel and carrier breakdowns', async () => { const prisma = createPrismaMock(); prisma.$queryRaw .mockResolvedValueOnce([{ signatureId: 'signature-1', signatureName: '【测试签名】', tenantId: 'tenant-1', tenantName: '租户A', applicationNames: '通知应用、营销应用', total: 5, acceptedCount: 4, submitFailureCount: 1, successCount: 3, unknownCount: 1, failureCount: 0, successRate: 75, averageArrivalMs: 1200, rowCount: 12, }]) .mockResolvedValueOnce([ { signatureId: 'signature-1', channelId: 'channel-1', channelName: '通道一', carrier: 'mobile', drainageState: 'with', total: 4, acceptedCount: 4, submitFailureCount: 0, successCount: 3, unknownCount: 1, failureCount: 0, successRate: 75, averageArrivalMs: 1200, }, { signatureId: 'signature-1', channelId: 'channel-2', channelName: '通道二', carrier: 'telecom', drainageState: 'without', total: 2, acceptedCount: 1, submitFailureCount: 1, successCount: 1, unknownCount: 0, failureCount: 0, successRate: 100, averageArrivalMs: 1800, }, ]); prisma.$queryRaw.mockResolvedValueOnce([ { signatureId: 'signature-1', carrier: 'mobile', businessMessageCount: 3, finalSuccessCount: 2, finalSuccessRate: 66.7, averageArrivalMs: 1100, }, { signatureId: 'signature-1', carrier: 'telecom', businessMessageCount: 2, finalSuccessCount: 1, finalSuccessRate: 50, averageArrivalMs: 1800, }, ]); const service = new OperationsService(prisma as never); await expect(service.signatureQuality({ date: '2026-07-24', keyword: '测试', page: 2, pageSize: 5, })).resolves.toEqual({ date: '2026-07-24', items: [expect.objectContaining({ signatureId: 'signature-1', signatureName: '【测试签名】', total: 5, channelSubmitTotal: 6, carrierOverview: [ expect.objectContaining({ carrier: 'mobile', businessMessageCount: 3, finalSuccessCount: 2, finalSuccessRate: 66.7, }), expect.objectContaining({ carrier: 'telecom', businessMessageCount: 2, finalSuccessCount: 1, finalSuccessRate: 50, }), ], breakdowns: [ expect.objectContaining({ channelId: 'channel-1', carrier: 'mobile', total: 4 }), expect.objectContaining({ channelId: 'channel-2', carrier: 'telecom', total: 2 }), ], drainageBreakdowns: [ expect.objectContaining({ channelId: 'channel-1', carrier: 'mobile', drainageState: 'with', total: 4 }), expect.objectContaining({ channelId: 'channel-2', carrier: 'telecom', drainageState: 'without', total: 2 }), ], })], total: 12, page: 2, pageSize: 5, }); expect(prisma.$queryRaw).toHaveBeenCalledTimes(3); }); it('does not query channel details when the selected date has no registered signatures', async () => { const prisma = createPrismaMock(); prisma.$queryRaw.mockResolvedValueOnce([]); const service = new OperationsService(prisma as never); await expect(service.signatureQuality({ date: '2026-07-24' })).resolves.toEqual({ date: '2026-07-24', items: [], total: 0, page: 1, pageSize: 10, }); expect(prisma.$queryRaw).toHaveBeenCalledTimes(1); }); it('returns trace details and reconciliation diffs', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.trace({ tenantId: 'tenant-1', taskId: 'task-1', messageId: 'MSG-1' })).resolves.toEqual({ messages: [{ messageId: 'MSG-1' }], billingRecords: [{ id: 'bill-1', messageId: 'MSG-1' }], uplinks: [{ id: 'uplink-1', messageId: 'MSG-1' }], }); await expect(service.reconciliation({ tenantId: 'tenant-1', taskId: 'task-1' })).resolves.toEqual( expect.objectContaining({ diff: { messageVsBillingAmountCents: 0, billingVsTransactionAmountCents: 0, messageVsBillingUnits: 0, }, }), ); }); it('returns paginated operation logs with normalized detail cards', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.systemLogs({ tenantId: 'tenant-1', keyword: '充值', page: 1, pageSize: 5 })).resolves.toEqual( expect.objectContaining({ total: 1, page: 1, pageSize: 5, modules: ['recharge_order'], items: [ expect.objectContaining({ level: 'success', tenant: '租户A', module: 'recharge_order', action: 'billing.manual_recharge', }), ], }), ); }); it('applies operation-log level filters before database pagination', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await service.systemLogs({ level: 'error', page: 2, pageSize: 5 }); expect(prisma.operationLog.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ AND: expect.objectContaining({ OR: expect.any(Array) }), }), skip: 5, take: 5, })); expect(prisma.operationLog.count).toHaveBeenCalledWith({ where: expect.objectContaining({ AND: expect.objectContaining({ OR: expect.any(Array) }) }), }); }); it('exports filtered operation logs with a traceable operation id', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.exportSystemLogs({ tenantId: 'tenant-1', range: '7d' })).resolves.toEqual(expect.objectContaining({ operationId: expect.any(String), status: 'completed', recordCount: 1, truncated: false, content: expect.stringContaining('billing.manual_recharge'), })); expect(prisma.operationLog.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 10_001 })); }); it('derives client log export tenant from session user and removes internal detail and IP columns', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); const exported = await service.exportSystemLogs({ tenantId: 'spoofed-tenant' }, 'client-user'); expect(prisma.user.findFirst).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ id: 'client-user' }) })); expect(prisma.operationLog.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ tenantId: 'tenant-1' }) })); expect(exported.content.split('\n')[0]).toBe('时间,级别,模块,操作人,动作,资源ID'); expect(exported.content).not.toContain('amountCents'); }); it('caps legacy audit-log reads with pagination', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.auditLogs({ page: 1, pageSize: 1_000 })).resolves.toEqual(expect.objectContaining({ total: 1, page: 1, pageSize: 100, })); expect(prisma.operationLog.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 100 })); }); it('returns paginated gateway submit dead letters', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.listGatewaySubmitDeadLetters({ tenantId: 'tenant-1', status: 'pending', keyword: 'SUBMIT', page: 1, pageSize: 10, })).resolves.toEqual({ items: [expect.objectContaining({ id: 'dead-1', status: 'pending', rawPayloadAvailable: true, commandPayload: { upstream: { account: 'sp', passwordCipher: '[REDACTED]' } }, })], total: 1, page: 1, pageSize: 10, summary: { pending: 1, requeueing: 0, requeued: 0, resolved: 0, oldestPendingAt: new Date('2026-07-08T12:00:00.000Z'), }, }); expect(prisma.gatewaySubmitDeadLetter.findMany).toHaveBeenCalledWith({ where: expect.objectContaining({ tenantId: 'tenant-1', status: 'pending', }), include: { tenant: true, application: true, channel: true }, orderBy: { createdAt: 'desc' }, skip: 0, take: 10, }); }); it('returns paginated receipt anomalies with status summary', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.listReceiptAnomalies({ tenantId: 'tenant-1', channelId: 'channel-1', status: 'pending', anomalyType: 'aggregate_success_then_failure', keyword: 'MSG-1', page: 1, pageSize: 10, })).resolves.toEqual(expect.objectContaining({ total: 1, page: 1, pageSize: 10, summary: { pending: 1, resolved: 0, ignored: 0, oldestPendingAt: new Date('2026-08-06T01:00:00.000Z'), }, })); expect(prisma.smsReceiptAnomaly.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ tenantId: 'tenant-1', channelId: 'channel-1', status: 'pending', anomalyType: 'aggregate_success_then_failure', }), orderBy: [{ lastOccurredAt: 'desc' }, { id: 'desc' }], take: 10, include: { tenant: { select: { id: true, name: true } }, application: { select: { id: true, name: true } }, channel: { select: { id: true, code: true, name: true, status: true } }, messageRecord: { select: { messageId: true, phoneNumber: true, status: true } }, submitRecord: { select: { submitId: true, submitStatus: true } }, receiptRecord: { select: { gatewayMessageId: true, receiptStatus: true, rawStatus: true, deliveredAt: true } }, }, })); }); it('returns paginated downstream deliveries', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.listDownstreamDeliveries({ tenantId: 'tenant-1', deliveryType: 'receipt', status: 'failed', keyword: '1380', createdAtFrom: '2026-07-01', createdAtTo: '2026-07-15', page: 1, pageSize: 10, })).resolves.toEqual({ items: [expect.objectContaining({ id: 'delivery-1', status: 'failed' })], total: 1, page: 1, pageSize: 10, }); expect(prisma.cmppDownstreamDelivery.findMany).toHaveBeenCalledWith({ where: expect.objectContaining({ tenantId: 'tenant-1', deliveryType: 'receipt', status: 'failed', createdAt: { gte: new Date('2026-07-01T00:00:00.000+08:00'), lte: new Date('2026-07-15T23:59:59.999+08:00'), }, }), include: { tenant: true, application: true, messageRecord: true, attempts: { orderBy: [{ attemptNo: 'desc' }, { createdAt: 'desc' }] }, }, orderBy: { createdAt: 'desc' }, skip: 0, take: 10, }); }); it('builds downstream delivery dashboard aggregates', async () => { const prisma = createPrismaMock(); prisma.cmppDownstreamDelivery.count = jest.fn() .mockResolvedValueOnce(12) .mockResolvedValueOnce(3) .mockResolvedValueOnce(0) .mockResolvedValueOnce(8) .mockResolvedValueOnce(1) .mockResolvedValueOnce(0) .mockResolvedValueOnce(0) .mockResolvedValueOnce(1) .mockResolvedValueOnce(0) .mockResolvedValueOnce(1) .mockResolvedValueOnce(2) .mockResolvedValueOnce(1) .mockResolvedValueOnce(0); prisma.cmppDownstreamDelivery.groupBy = jest.fn() .mockResolvedValueOnce([ { deliveryType: 'receipt', status: 'pending', _count: { _all: 2 } }, { deliveryType: 'receipt', status: 'failed', _count: { _all: 1 } }, { deliveryType: 'receipt', status: 'delivered', _count: { _all: 6 } }, { deliveryType: 'uplink', status: 'pending', _count: { _all: 1 } }, { deliveryType: 'uplink', status: 'delivered', _count: { _all: 2 } }, ]) .mockResolvedValueOnce([ { applicationId: 'app-1', status: 'pending', _count: { _all: 2 } }, { applicationId: 'app-1', status: 'failed', _count: { _all: 1 } }, { applicationId: 'app-1', status: 'delivered', _count: { _all: 5 } }, { applicationId: 'app-2', status: 'pending', _count: { _all: 1 } }, { applicationId: 'app-2', status: 'delivered', _count: { _all: 3 } }, ]) .mockResolvedValueOnce([ { applicationId: 'app-1', _count: { _all: 1 } }, { applicationId: 'app-2', _count: { _all: 1 } }, ]); const service = new OperationsService(prisma as never); await expect(service.downstreamDeliveryDashboard({ tenantId: 'tenant-1', applicationId: 'app-1', deliveryType: 'all', })).resolves.toEqual({ summary: { total: 12, pending: 3, awaitingAck: 0, delivered: 8, failed: 1, unconfirmed: 0, rejected: 0, stalledPending: 1, stalledAck: 0, recentFailed: 1, alertCount: 2, }, typeBreakdown: [ { deliveryType: 'receipt', total: 9, pending: 2, awaitingAck: 0, delivered: 6, failed: 1, unconfirmed: 0, rejected: 0 }, { deliveryType: 'uplink', total: 3, pending: 1, awaitingAck: 0, delivered: 2, failed: 0, unconfirmed: 0, rejected: 0 }, ], retryBuckets: [ { label: '0次', count: 2 }, { label: '1-3次', count: 1 }, { label: '4次及以上', count: 0 }, ], topApplications: [ { applicationId: 'app-1', name: '应用A', pending: 2, awaitingAck: 0, failed: 1, unconfirmed: 0, rejected: 0, delivered: 5, alertCount: 1 }, { applicationId: 'app-2', name: '应用B', pending: 1, awaitingAck: 0, failed: 0, unconfirmed: 0, rejected: 0, delivered: 3, alertCount: 1 }, ], }); expect(prisma.cmppDownstreamDelivery.groupBy).toHaveBeenNthCalledWith(3, { by: ['applicationId'], where: { AND: [ { tenantId: 'tenant-1', applicationId: 'app-1', deliveryType: undefined, createdAt: undefined }, { OR: [ { status: 'pending', OR: [ { lastRetriedAt: null, createdAt: { lte: expect.any(Date) } }, { lastRetriedAt: { lte: expect.any(Date) } }, ], }, { status: 'awaiting_ack', ackDeadlineAt: { lte: expect.any(Date) } }, { status: { in: ['failed', 'unconfirmed', 'rejected'] }, updatedAt: { gte: expect.any(Date) } }, ], }, ], }, _count: { _all: true }, }); }); it('returns paginated downstream recovery statuses', async () => { const prisma = createPrismaMock(); prisma.gatewayDownstreamRecoveryStatus.count = jest.fn() .mockResolvedValueOnce(1) .mockResolvedValueOnce(0) .mockResolvedValueOnce(0) .mockResolvedValueOnce(0) .mockResolvedValueOnce(1) .mockResolvedValueOnce(1); const service = new OperationsService(prisma as never); await expect(service.listDownstreamRecoveryStatuses({ tenantId: 'tenant-1', applicationId: 'app-1', state: 'waiting_connection', failureCategory: 'client_disconnected', keyword: '100001', updatedAtFrom: '2026-07-02', updatedAtTo: '2026-07-08', page: 1, pageSize: 10, })).resolves.toEqual({ items: [expect.objectContaining({ id: 'recover-1', account: '100001', state: 'waiting_connection' })], total: 1, page: 1, pageSize: 10, summary: { total: 1, running: 0, success: 0, failed: 0, waitingConnection: 1, backoff: 1, failureCategories: [ { category: 'client_disconnected', count: 1 }, ], }, }); expect(prisma.gatewayDownstreamRecoveryStatus.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ updatedAt: { gte: new Date('2026-07-01T16:00:00.000Z'), lte: new Date('2026-07-08T15:59:59.999Z'), }, }), })); }); it('returns downstream recovery status detail', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.getDownstreamRecoveryStatus('recover-1')).resolves.toEqual( expect.objectContaining({ id: 'recover-1', account: '100001', gatewayInstanceId: 'gw-01', }), ); expect(prisma.gatewayDownstreamRecoveryStatus.findUnique).toHaveBeenCalledWith({ where: { id: 'recover-1' }, include: { tenant: true, application: true }, }); }); it('exports downstream recovery statuses as csv rows', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.exportDownstreamRecoveryStatuses({ tenantId: 'tenant-1', state: 'waiting_connection', keyword: '100001', updatedAtFrom: '2026-07-02', updatedAtTo: '2026-07-08', })).resolves.toEqual(expect.objectContaining({ total: 1, fileName: expect.stringMatching(/^gateway-downstream-recovery-statuses-\d{8}-\d{6}\.csv$/), content: expect.stringContaining('100001'), })); expect(prisma.gatewayDownstreamRecoveryStatus.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ failureCategory: undefined, updatedAt: { gte: new Date('2026-07-01T16:00:00.000Z'), lte: new Date('2026-07-08T15:59:59.999Z'), }, }), })); }); it('returns message segment audit rows', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.listMessageSegmentAudits({ messageRecordId: 'record-1' })).resolves.toEqual([ expect.objectContaining({ id: 'segment-1', segmentIndex: 1, gatewayMessageId: 'GW-1-A', }), ]); expect(prisma.smsMessageSegmentAudit.findMany).toHaveBeenCalledWith({ where: { messageRecordId: 'record-1', messageRecord: undefined, }, include: { channel: true, submitRecord: true }, orderBy: [{ createdAt: 'asc' }, { segmentIndex: 'asc' }, { id: 'asc' }], }); }); });