173 lines
6.1 KiB
TypeScript
173 lines
6.1 KiB
TypeScript
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',
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
});
|