fix: close sms scheduling and billing gaps
This commit is contained in:
@@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ChannelsService,
|
||||
ChangeChannelStatusDto,
|
||||
CreateChannelDto,
|
||||
CreateChannelGroupDto,
|
||||
CreateChannelGroupItemDto,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
CreateReportMaterialDto,
|
||||
CreateReportTaskDto,
|
||||
CreateRouteRuleDto,
|
||||
UpsertConnectionStateDto,
|
||||
} from './channels.service';
|
||||
|
||||
@ApiTags('channels')
|
||||
@@ -33,11 +35,31 @@ export class ChannelsController {
|
||||
return this.channels.testChannel(channelId);
|
||||
}
|
||||
|
||||
@Post('channels/:id/status')
|
||||
changeChannelStatus(@Param('id') channelId: string, @Body() body: ChangeChannelStatusDto) {
|
||||
return this.channels.changeChannelStatus(channelId, body);
|
||||
}
|
||||
|
||||
@Get('channels/:id/metrics')
|
||||
listChannelMetrics(@Param('id') channelId: string) {
|
||||
return this.channels.listChannelMetrics(channelId);
|
||||
}
|
||||
|
||||
@Get('channels/:id/connections')
|
||||
listChannelConnections(@Param('id') channelId: string) {
|
||||
return this.channels.listChannelConnections(channelId);
|
||||
}
|
||||
|
||||
@Get('tenants/:id/connections')
|
||||
listTenantConnections(@Param('id') tenantId: string) {
|
||||
return this.channels.listTenantConnections(tenantId);
|
||||
}
|
||||
|
||||
@Post('gateway/connections')
|
||||
upsertConnectionState(@Body() body: UpsertConnectionStateDto) {
|
||||
return this.channels.upsertConnectionState(body);
|
||||
}
|
||||
|
||||
@Get('channel-groups')
|
||||
listGroups() {
|
||||
return this.channels.listGroups();
|
||||
|
||||
@@ -6,6 +6,8 @@ function createPrismaMock() {
|
||||
smsChannel: {
|
||||
findMany: jest.fn(),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'channel-1', ...data })),
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'channel-1', status: 'active' }),
|
||||
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'channel-1', ...data })),
|
||||
},
|
||||
channelHealthMetric: { findMany: jest.fn() },
|
||||
smsChannelGroup: {
|
||||
@@ -46,6 +48,13 @@ function createPrismaMock() {
|
||||
smsSignature: {
|
||||
update: jest.fn(),
|
||||
},
|
||||
cmppConnectionState: {
|
||||
findMany: jest.fn(),
|
||||
upsert: jest.fn().mockImplementation(({ create }) => Promise.resolve({ id: 'conn-1', ...create })),
|
||||
},
|
||||
operationLog: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,4 +143,54 @@ describe('ChannelsService', () => {
|
||||
data: { reportStatus: 'rejected' },
|
||||
});
|
||||
});
|
||||
|
||||
it('updates channel status with operation logs', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new ChannelsService(prisma as never);
|
||||
|
||||
await service.changeChannelStatus('channel-1', { status: 'disabled', operatorId: 'admin-1', reason: 'maintenance' });
|
||||
|
||||
expect(prisma.smsChannel.update).toHaveBeenCalledWith({ where: { id: 'channel-1' }, data: { status: 'disabled' } });
|
||||
expect(prisma.operationLog.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
userId: 'admin-1',
|
||||
action: 'sms_channel.disabled',
|
||||
resource: 'sms_channel',
|
||||
resourceId: 'channel-1',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('upserts and lists CMPP connection states', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new ChannelsService(prisma as never);
|
||||
|
||||
await service.upsertConnectionState({
|
||||
tenantId: 'tenant-1',
|
||||
channelId: 'channel-1',
|
||||
connectionId: 'conn-a',
|
||||
status: 'online',
|
||||
desiredConnections: 2,
|
||||
currentConnections: 1,
|
||||
});
|
||||
await service.listChannelConnections('channel-1');
|
||||
await service.listTenantConnections('tenant-1');
|
||||
|
||||
expect(prisma.cmppConnectionState.upsert).toHaveBeenCalledWith({
|
||||
where: { channelId_connectionId: { channelId: 'channel-1', connectionId: 'conn-a' } },
|
||||
update: expect.objectContaining({ tenantId: 'tenant-1', status: 'online', desiredConnections: 2, currentConnections: 1 }),
|
||||
create: expect.objectContaining({ channelId: 'channel-1', connectionId: 'conn-a', status: 'online' }),
|
||||
});
|
||||
expect(prisma.cmppConnectionState.findMany).toHaveBeenCalledWith({
|
||||
where: { channelId: 'channel-1' },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
expect(prisma.cmppConnectionState.findMany).toHaveBeenCalledWith({
|
||||
where: { tenantId: 'tenant-1' },
|
||||
include: { channel: true },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,6 +92,26 @@ export interface CreateReceiptImportDto {
|
||||
result?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface UpsertConnectionStateDto {
|
||||
tenantId?: string;
|
||||
channelId: string;
|
||||
connectionId: string;
|
||||
status: string;
|
||||
desiredConnections?: number;
|
||||
currentConnections?: number;
|
||||
lastConnectedAt?: string;
|
||||
lastDisconnectedAt?: string;
|
||||
lastHeartbeatAt?: string;
|
||||
reconnectCount?: number;
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
export interface ChangeChannelStatusDto {
|
||||
status: string;
|
||||
operatorId?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ChannelsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
@@ -122,6 +142,28 @@ export class ChannelsService {
|
||||
});
|
||||
}
|
||||
|
||||
async changeChannelStatus(channelId: string, data: ChangeChannelStatusDto) {
|
||||
const channel = await this.prisma.smsChannel.findUnique({ where: { id: channelId } });
|
||||
if (!channel) {
|
||||
throw new NotFoundException('Channel not found');
|
||||
}
|
||||
const updated = await this.prisma.smsChannel.update({ where: { id: channelId }, data: { status: data.status } });
|
||||
await this.prisma.operationLog.create({
|
||||
data: {
|
||||
userId: data.operatorId,
|
||||
action: `sms_channel.${data.status}`,
|
||||
resource: 'sms_channel',
|
||||
resourceId: channelId,
|
||||
detail: {
|
||||
statusBefore: channel.status,
|
||||
statusAfter: data.status,
|
||||
reason: data.reason,
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
testChannel(channelId: string) {
|
||||
return {
|
||||
channelId,
|
||||
@@ -138,6 +180,46 @@ export class ChannelsService {
|
||||
});
|
||||
}
|
||||
|
||||
listChannelConnections(channelId: string) {
|
||||
return this.prisma.cmppConnectionState.findMany({
|
||||
where: { channelId },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
}
|
||||
|
||||
listTenantConnections(tenantId: string) {
|
||||
return this.prisma.cmppConnectionState.findMany({
|
||||
where: { tenantId },
|
||||
include: { channel: true },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
}
|
||||
|
||||
upsertConnectionState(data: UpsertConnectionStateDto) {
|
||||
const payload = {
|
||||
tenantId: data.tenantId,
|
||||
status: data.status,
|
||||
desiredConnections: data.desiredConnections ?? 1,
|
||||
currentConnections: data.currentConnections ?? (data.status === 'online' || data.status === 'connected' ? 1 : 0),
|
||||
lastConnectedAt: data.lastConnectedAt ? new Date(data.lastConnectedAt) : undefined,
|
||||
lastDisconnectedAt: data.lastDisconnectedAt ? new Date(data.lastDisconnectedAt) : undefined,
|
||||
lastHeartbeatAt: data.lastHeartbeatAt ? new Date(data.lastHeartbeatAt) : undefined,
|
||||
reconnectCount: data.reconnectCount ?? 0,
|
||||
lastError: data.lastError,
|
||||
};
|
||||
return this.prisma.cmppConnectionState.upsert({
|
||||
where: { channelId_connectionId: { channelId: data.channelId, connectionId: data.connectionId } },
|
||||
update: payload,
|
||||
create: {
|
||||
channelId: data.channelId,
|
||||
connectionId: data.connectionId,
|
||||
...payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
listGroups() {
|
||||
return this.prisma.smsChannelGroup.findMany({
|
||||
include: { items: { include: { channel: true } } },
|
||||
|
||||
Reference in New Issue
Block a user