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']); }); });