Files
lislgosms/api/src/tenants/tenants.service.spec.ts
T

189 lines
7.4 KiB
TypeScript

import { BadRequestException, NotFoundException } from '@nestjs/common';
import { TenantsService } from './tenants.service';
function createPrismaMock() {
const tenant = {
id: 'tenant-1',
name: '测试企业',
code: 'TENANT001',
status: 'active',
createdAt: new Date('2026-07-03T00:00:00.000Z'),
updatedAt: new Date('2026-07-03T00:00:00.000Z'),
};
const prisma = {
$executeRaw: jest.fn().mockResolvedValue(1),
tenant: {
findMany: jest.fn().mockResolvedValue([{ ...tenant, enterpriseCertifications: [] }]),
findUnique: jest.fn().mockResolvedValue({ ...tenant, enterpriseCertifications: [] }),
create: jest.fn().mockResolvedValue(tenant),
update: jest.fn().mockResolvedValue(tenant),
},
smsApplication: {
count: jest.fn().mockResolvedValue(0),
},
enterpriseCertification: {
findFirst: jest.fn().mockResolvedValue(null),
create: jest.fn().mockResolvedValue({ id: 'cert-1' }),
update: jest.fn().mockResolvedValue({ id: 'cert-1' }),
},
tenantAccount: {
findMany: jest.fn().mockResolvedValue([{ tenantId: 'tenant-1', balanceCents: 12000, status: 'active' }]),
findUnique: jest.fn().mockResolvedValue({ balanceCents: 0 }),
},
smsMessageRecord: {
groupBy: jest.fn().mockResolvedValue([{ tenantId: 'tenant-1', _sum: { amountCents: 350 } }]),
},
accountTransaction: {
groupBy: jest.fn().mockResolvedValue([{ tenantId: 'tenant-1', _sum: { amountCents: 125 } }]),
},
};
return {
...prisma,
$transaction: jest.fn((callback: (tx: typeof prisma) => unknown) => callback(prisma)),
};
}
describe('TenantsService', () => {
it('creates tenants with a real enterprise profile', async () => {
const prisma = createPrismaMock();
const service = new TenantsService(prisma as never);
await service.create({
name: '测试企业',
code: 'TENANT001',
creditCode: '91370000123456789X',
province: '山东',
city: '济南',
address: '历下区测试路 1 号',
contactName: '张三',
contactIdCard: '370100199001010011',
contactPhone: '13800000000',
contactEmail: 'contact@example.com',
});
expect(prisma.enterpriseCertification.create).toHaveBeenCalledWith({
data: expect.objectContaining({
tenantId: 'tenant-1',
companyName: '测试企业',
licenseNo: '91370000123456789X',
contactName: '张三',
contactPhone: '13800000000',
status: 'approved',
materials: expect.objectContaining({
province: '山东',
city: '济南',
address: '历下区测试路 1 号',
contactIdCard: '370100199001010011',
contactEmail: 'contact@example.com',
}),
}),
});
});
it('throws 404 when updating or deleting a missing tenant', async () => {
const prisma = createPrismaMock();
prisma.tenant.findUnique.mockResolvedValue(null);
const service = new TenantsService(prisma as never);
await expect(service.update('missing', { name: '不存在' })).rejects.toBeInstanceOf(NotFoundException);
await expect(service.delete('missing')).rejects.toBeInstanceOf(NotFoundException);
expect(prisma.tenant.update).not.toHaveBeenCalled();
});
it('blocks enterprise deletion while applications are active or disabling', async () => {
const prisma = createPrismaMock();
prisma.smsApplication.count.mockResolvedValue(2);
const service = new TenantsService(prisma as never);
await expect(service.delete('tenant-1')).rejects.toThrow('还有 2 个启用或停用中的企业应用');
expect(prisma.tenant.update).not.toHaveBeenCalled();
expect(prisma.smsApplication.count).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1', status: { in: ['active', 'disabling'] } },
});
});
it('blocks enterprise deletion until a non-zero account balance is cleared', async () => {
const prisma = createPrismaMock();
prisma.tenantAccount.findUnique.mockResolvedValue({ balanceCents: -1250 });
const service = new TenantsService(prisma as never);
await expect(service.delete('tenant-1')).rejects.toThrow('完成余额清算后方可删除,请给企业充值到金额为0');
expect(prisma.tenant.update).not.toHaveBeenCalled();
expect(prisma.smsApplication.count).not.toHaveBeenCalled();
expect(prisma.$executeRaw).toHaveBeenCalledTimes(1);
});
it('allows enterprise deletion only when the account balance is zero and no applications are active', async () => {
const prisma = createPrismaMock();
prisma.tenantAccount.findUnique.mockResolvedValue({ balanceCents: 0 });
const service = new TenantsService(prisma as never);
await expect(service.delete('tenant-1')).resolves.toMatchObject({ id: 'tenant-1' });
expect(prisma.tenant.update).toHaveBeenCalledWith({
where: { id: 'tenant-1' },
data: { status: 'deleted' },
});
});
it('rejects enterprise credit codes containing non-alphanumeric characters', async () => {
const prisma = createPrismaMock();
const service = new TenantsService(prisma as never);
await expect(service.create({ name: '测试企业', creditCode: '9137-中文' })).rejects.toBeInstanceOf(BadRequestException);
expect(prisma.tenant.create).not.toHaveBeenCalled();
});
it('lists management rows with real account, today spend and actual refund fields', async () => {
const prisma = createPrismaMock();
const service = new TenantsService(prisma as never);
await expect(service.listManagementRows()).resolves.toEqual([
expect.objectContaining({
id: 'tenant-1',
name: '测试企业',
account: expect.objectContaining({ balanceCents: 12000 }),
todaySpendCents: 350,
todayRefundCents: 125,
}),
]);
expect(prisma.tenant.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: { status: { not: 'deleted' } },
}));
expect(prisma.tenant.findMany.mock.calls[0][0]).not.toHaveProperty('take');
expect(prisma.tenantAccount.findMany).toHaveBeenCalledWith();
expect(prisma.smsMessageRecord.groupBy).toHaveBeenCalledWith(expect.objectContaining({
by: ['tenantId'],
_sum: { amountCents: true },
}));
expect(prisma.accountTransaction.groupBy).toHaveBeenCalledWith(expect.objectContaining({
by: ['tenantId'],
where: expect.objectContaining({
OR: [
{ transactionType: 'refunded' },
{ transactionType: 'released', relatedType: 'sms_message_record' },
],
}),
_sum: { amountCents: true },
}));
});
it('sorts management rows by today spend descending with a stable name tie-breaker', async () => {
const prisma = createPrismaMock();
prisma.tenant.findMany.mockResolvedValue([
{ id: 'tenant-1', name: '乙企业', code: 'T1', status: 'active', enterpriseCertifications: [] },
{ id: 'tenant-2', name: '甲企业', code: 'T2', status: 'active', enterpriseCertifications: [] },
{ id: 'tenant-3', name: '丙企业', code: 'T3', status: 'active', enterpriseCertifications: [] },
]);
prisma.smsMessageRecord.groupBy.mockResolvedValue([
{ tenantId: 'tenant-1', _sum: { amountCents: 300 } },
{ tenantId: 'tenant-2', _sum: { amountCents: 300 } },
{ tenantId: 'tenant-3', _sum: { amountCents: 500 } },
]);
const service = new TenantsService(prisma as never);
const rows = await service.listManagementRows();
expect(rows.map((row) => row.id)).toEqual(['tenant-3', 'tenant-2', 'tenant-1']);
});
});