189 lines
7.4 KiB
TypeScript
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']);
|
|
});
|
|
});
|