import { OperationsService } from './operations.service'; function createPrismaMock() { return { smsBatchTask: { findMany: jest.fn().mockResolvedValue([{ id: 'task-1', taskNo: 'BATCH-1' }]), count: jest.fn().mockResolvedValue(3), }, smsMessageRecord: { findMany: jest.fn().mockResolvedValue([{ messageId: 'MSG-1' }]), 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, smsUnits: -2 } }), }, 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), }, enterpriseCertification: { count: jest.fn().mockResolvedValue(1), }, 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 } }]), }, }; } describe('OperationsService', () => { it('filters send-chain messages by tenant, application, channel, 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', taskId: 'task-1', messageId: 'MSG-1', phoneNumber: '13800000001', status: 'delivered', }); expect(prisma.smsMessageRecord.findMany).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', applicationId: 'app-1', channelId: 'channel-1', batchTaskId: 'task-1', messageId: 'MSG-1', phoneNumber: '13800000001', status: 'delivered', }, include: { tenant: true, application: true, channel: true, submitRecords: true, receiptRecords: true }, orderBy: { queuedAt: 'desc' }, take: 500, }); }); 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' }, include: { tenant: true, channel: true }, orderBy: { receivedAt: 'desc' }, take: 500, }); }); it('builds dashboard and statistics aggregates', async () => { const prisma = createPrismaMock(); const service = new OperationsService(prisma as never); await expect(service.dashboard({ tenantId: 'tenant-1' })).resolves.toEqual( expect.objectContaining({ taskCount: 3, uplinkCount: 1, pendingAuditCount: 6, gatewayConnections: [{ status: 'connected', _count: { _all: 1 }, _sum: { currentConnections: 2, desiredConnections: 2 } }], }), ); 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 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', }), ], }), ); }); });