test: Add unit tests for commissions and sales modules

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 01:33:15 -06:00
parent eb6a83daba
commit a2a1fd3d3b
7 changed files with 2495 additions and 0 deletions

View File

@ -0,0 +1,328 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotFoundException } from '@nestjs/common';
import { AssignmentsService } from '../services/assignments.service';
import { CommissionAssignmentEntity, CommissionSchemeEntity, SchemeType, AppliesTo } from '../entities';
describe('AssignmentsService', () => {
let service: AssignmentsService;
let assignmentRepo: jest.Mocked<Repository<CommissionAssignmentEntity>>;
let schemeRepo: jest.Mocked<Repository<CommissionSchemeEntity>>;
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
const mockSchemeId = '550e8400-e29b-41d4-a716-446655440003';
const mockScheme: Partial<CommissionSchemeEntity> = {
id: mockSchemeId,
tenantId: mockTenantId,
name: 'Standard Commission',
type: SchemeType.PERCENTAGE,
rate: 10,
isActive: true,
deletedAt: null,
};
const mockAssignment: Partial<CommissionAssignmentEntity> = {
id: 'assignment-001',
tenantId: mockTenantId,
userId: mockUserId,
schemeId: mockSchemeId,
startsAt: new Date('2026-01-01'),
endsAt: null,
customRate: null,
isActive: true,
createdAt: new Date('2026-01-01'),
createdBy: mockUserId,
scheme: mockScheme as CommissionSchemeEntity,
};
beforeEach(async () => {
const mockAssignmentRepo = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
remove: jest.fn(),
createQueryBuilder: jest.fn(),
};
const mockSchemeRepo = {
findOne: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
AssignmentsService,
{ provide: getRepositoryToken(CommissionAssignmentEntity), useValue: mockAssignmentRepo },
{ provide: getRepositoryToken(CommissionSchemeEntity), useValue: mockSchemeRepo },
],
}).compile();
service = module.get<AssignmentsService>(AssignmentsService);
assignmentRepo = module.get(getRepositoryToken(CommissionAssignmentEntity));
schemeRepo = module.get(getRepositoryToken(CommissionSchemeEntity));
});
afterEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
it('should create an assignment successfully', async () => {
schemeRepo.findOne.mockResolvedValue(mockScheme as CommissionSchemeEntity);
assignmentRepo.create.mockReturnValue(mockAssignment as CommissionAssignmentEntity);
assignmentRepo.save.mockResolvedValue(mockAssignment as CommissionAssignmentEntity);
const dto = {
userId: mockUserId,
schemeId: mockSchemeId,
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.userId).toBe(mockUserId);
expect(result.schemeId).toBe(mockSchemeId);
expect(result.isActive).toBe(true);
expect(schemeRepo.findOne).toHaveBeenCalled();
expect(assignmentRepo.create).toHaveBeenCalled();
expect(assignmentRepo.save).toHaveBeenCalled();
});
it('should create an assignment with custom rate', async () => {
const customRateAssignment = { ...mockAssignment, customRate: 15 };
schemeRepo.findOne.mockResolvedValue(mockScheme as CommissionSchemeEntity);
assignmentRepo.create.mockReturnValue(customRateAssignment as CommissionAssignmentEntity);
assignmentRepo.save.mockResolvedValue(customRateAssignment as CommissionAssignmentEntity);
const dto = {
userId: mockUserId,
schemeId: mockSchemeId,
customRate: 15,
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.customRate).toBe(15);
});
it('should create an assignment with date range', async () => {
const datedAssignment = {
...mockAssignment,
startsAt: new Date('2026-01-01'),
endsAt: new Date('2026-12-31'),
};
schemeRepo.findOne.mockResolvedValue(mockScheme as CommissionSchemeEntity);
assignmentRepo.create.mockReturnValue(datedAssignment as CommissionAssignmentEntity);
assignmentRepo.save.mockResolvedValue(datedAssignment as CommissionAssignmentEntity);
const dto = {
userId: mockUserId,
schemeId: mockSchemeId,
startsAt: '2026-01-01',
endsAt: '2026-12-31',
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.startsAt).toBeDefined();
expect(result.endsAt).toBeDefined();
});
it('should throw NotFoundException if scheme not found', async () => {
schemeRepo.findOne.mockResolvedValue(null);
const dto = {
userId: mockUserId,
schemeId: 'invalid-scheme-id',
};
await expect(service.create(mockTenantId, mockUserId, dto)).rejects.toThrow(
NotFoundException,
);
});
});
describe('findAll', () => {
it('should return paginated assignments', async () => {
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockAssignment], 1]),
};
assignmentRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.findAll(mockTenantId, { page: 1, limit: 10 });
expect(result.items).toHaveLength(1);
expect(result.total).toBe(1);
expect(result.page).toBe(1);
});
it('should filter by userId', async () => {
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockAssignment], 1]),
};
assignmentRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
await service.findAll(mockTenantId, { userId: mockUserId });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'a.user_id = :userId',
{ userId: mockUserId },
);
});
it('should filter by schemeId', async () => {
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockAssignment], 1]),
};
assignmentRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
await service.findAll(mockTenantId, { schemeId: mockSchemeId });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'a.scheme_id = :schemeId',
{ schemeId: mockSchemeId },
);
});
});
describe('findOne', () => {
it('should return an assignment by id', async () => {
assignmentRepo.findOne.mockResolvedValue(mockAssignment as CommissionAssignmentEntity);
const result = await service.findOne(mockTenantId, 'assignment-001');
expect(result.id).toBe('assignment-001');
expect(result.userId).toBe(mockUserId);
});
it('should throw NotFoundException if assignment not found', async () => {
assignmentRepo.findOne.mockResolvedValue(null);
await expect(service.findOne(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
describe('update', () => {
it('should update an assignment successfully', async () => {
const updatedAssignment = { ...mockAssignment, customRate: 20 };
assignmentRepo.findOne.mockResolvedValue(mockAssignment as CommissionAssignmentEntity);
assignmentRepo.save.mockResolvedValue(updatedAssignment as CommissionAssignmentEntity);
const result = await service.update(mockTenantId, 'assignment-001', { customRate: 20 });
expect(result.customRate).toBe(20);
});
it('should update dates', async () => {
const updatedAssignment = {
...mockAssignment,
endsAt: new Date('2026-06-30'),
};
assignmentRepo.findOne.mockResolvedValue(mockAssignment as CommissionAssignmentEntity);
assignmentRepo.save.mockResolvedValue(updatedAssignment as CommissionAssignmentEntity);
const result = await service.update(mockTenantId, 'assignment-001', {
endsAt: '2026-06-30',
});
expect(result.endsAt).toBeDefined();
});
it('should throw NotFoundException if assignment not found', async () => {
assignmentRepo.findOne.mockResolvedValue(null);
await expect(
service.update(mockTenantId, 'invalid-id', { customRate: 20 }),
).rejects.toThrow(NotFoundException);
});
});
describe('remove', () => {
it('should delete an assignment', async () => {
assignmentRepo.findOne.mockResolvedValue(mockAssignment as CommissionAssignmentEntity);
await service.remove(mockTenantId, 'assignment-001');
expect(assignmentRepo.remove).toHaveBeenCalledWith(mockAssignment);
});
it('should throw NotFoundException if assignment not found', async () => {
assignmentRepo.findOne.mockResolvedValue(null);
await expect(service.remove(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
describe('findActiveForUser', () => {
it('should return active assignments for a user', async () => {
const now = new Date();
const activeAssignment = {
...mockAssignment,
startsAt: new Date(now.getTime() - 86400000), // yesterday
endsAt: new Date(now.getTime() + 86400000), // tomorrow
};
assignmentRepo.find.mockResolvedValue([activeAssignment as CommissionAssignmentEntity]);
const result = await service.findActiveForUser(mockTenantId, mockUserId);
expect(result).toHaveLength(1);
expect(result[0].userId).toBe(mockUserId);
});
it('should filter out expired assignments', async () => {
const now = new Date();
const expiredAssignment = {
...mockAssignment,
startsAt: new Date(now.getTime() - 172800000), // 2 days ago
endsAt: new Date(now.getTime() - 86400000), // yesterday
};
assignmentRepo.find.mockResolvedValue([expiredAssignment as CommissionAssignmentEntity]);
const result = await service.findActiveForUser(mockTenantId, mockUserId);
expect(result).toHaveLength(0);
});
it('should include assignments without end date', async () => {
const now = new Date();
const openEndedAssignment = {
...mockAssignment,
startsAt: new Date(now.getTime() - 86400000), // yesterday
endsAt: null,
};
assignmentRepo.find.mockResolvedValue([openEndedAssignment as CommissionAssignmentEntity]);
const result = await service.findActiveForUser(mockTenantId, mockUserId);
expect(result).toHaveLength(1);
});
});
});

View File

@ -0,0 +1,338 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { CommissionsDashboardService } from '../services/commissions-dashboard.service';
import {
CommissionSchemeEntity,
CommissionAssignmentEntity,
CommissionEntryEntity,
CommissionPeriodEntity,
EntryStatus,
PeriodStatus,
} from '../entities';
describe('CommissionsDashboardService', () => {
let service: CommissionsDashboardService;
let schemeRepo: jest.Mocked<Repository<CommissionSchemeEntity>>;
let assignmentRepo: jest.Mocked<Repository<CommissionAssignmentEntity>>;
let entryRepo: jest.Mocked<Repository<CommissionEntryEntity>>;
let periodRepo: jest.Mocked<Repository<CommissionPeriodEntity>>;
let dataSource: jest.Mocked<DataSource>;
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
beforeEach(async () => {
const mockSchemeRepo = {
count: jest.fn(),
};
const mockAssignmentRepo = {
count: jest.fn(),
};
const mockEntryRepo = {
createQueryBuilder: jest.fn(),
};
const mockPeriodRepo = {
findOne: jest.fn(),
};
const mockDataSource = {
query: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
CommissionsDashboardService,
{ provide: getRepositoryToken(CommissionSchemeEntity), useValue: mockSchemeRepo },
{ provide: getRepositoryToken(CommissionAssignmentEntity), useValue: mockAssignmentRepo },
{ provide: getRepositoryToken(CommissionEntryEntity), useValue: mockEntryRepo },
{ provide: getRepositoryToken(CommissionPeriodEntity), useValue: mockPeriodRepo },
{ provide: DataSource, useValue: mockDataSource },
],
}).compile();
service = module.get<CommissionsDashboardService>(CommissionsDashboardService);
schemeRepo = module.get(getRepositoryToken(CommissionSchemeEntity));
assignmentRepo = module.get(getRepositoryToken(CommissionAssignmentEntity));
entryRepo = module.get(getRepositoryToken(CommissionEntryEntity));
periodRepo = module.get(getRepositoryToken(CommissionPeriodEntity));
dataSource = module.get(DataSource);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getDashboardSummary', () => {
it('should return complete dashboard summary', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([
{ status: EntryStatus.PENDING, count: 10, total: 1000 },
{ status: EntryStatus.APPROVED, count: 5, total: 500 },
{ status: EntryStatus.PAID, count: 15, total: 1500 },
]),
};
const mockPeriod = {
id: 'period-001',
name: 'January 2026',
status: PeriodStatus.OPEN,
};
schemeRepo.count.mockResolvedValue(3);
assignmentRepo.count.mockResolvedValue(10);
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
periodRepo.findOne.mockResolvedValue(mockPeriod as CommissionPeriodEntity);
const result = await service.getDashboardSummary(mockTenantId);
expect(result.totalSchemes).toBe(3);
expect(result.totalActiveAssignments).toBe(10);
expect(result.pendingCommissions).toBe(10);
expect(result.pendingAmount).toBe(1000);
expect(result.approvedAmount).toBe(500);
expect(result.paidAmount).toBe(1500);
expect(result.currentPeriod).toBe('January 2026');
expect(result.currency).toBe('USD');
});
it('should handle no entries', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([]),
};
schemeRepo.count.mockResolvedValue(0);
assignmentRepo.count.mockResolvedValue(0);
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
periodRepo.findOne.mockResolvedValue(null);
const result = await service.getDashboardSummary(mockTenantId);
expect(result.totalSchemes).toBe(0);
expect(result.pendingCommissions).toBe(0);
expect(result.pendingAmount).toBe(0);
expect(result.currentPeriod).toBeNull();
});
});
describe('getUserEarnings', () => {
it('should return user earnings summary', async () => {
dataSource.query.mockResolvedValue([{
total_pending: 500,
total_approved: 300,
total_paid: 1200,
total_entries: 20,
currency: 'USD',
}]);
const result = await service.getUserEarnings(mockTenantId, mockUserId);
expect(result.totalPending).toBe(500);
expect(result.totalApproved).toBe(300);
expect(result.totalPaid).toBe(1200);
expect(result.totalEntries).toBe(20);
expect(result.currency).toBe('USD');
});
it('should return zero values when no earnings', async () => {
dataSource.query.mockResolvedValue([]);
const result = await service.getUserEarnings(mockTenantId, mockUserId);
expect(result.totalPending).toBe(0);
expect(result.totalApproved).toBe(0);
expect(result.totalPaid).toBe(0);
expect(result.totalEntries).toBe(0);
});
it('should filter by date range', async () => {
dataSource.query.mockResolvedValue([{
total_pending: 100,
total_approved: 200,
total_paid: 300,
total_entries: 5,
currency: 'USD',
}]);
const startDate = new Date('2026-01-01');
const endDate = new Date('2026-01-31');
await service.getUserEarnings(mockTenantId, mockUserId, startDate, endDate);
expect(dataSource.query).toHaveBeenCalledWith(
`SELECT * FROM commissions.get_user_earnings($1, $2, $3, $4)`,
[mockUserId, mockTenantId, startDate, endDate],
);
});
});
describe('getEntriesByStatus', () => {
it('should return entries grouped by status', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([
{ status: EntryStatus.PENDING, count: 10, totalAmount: 1000 },
{ status: EntryStatus.APPROVED, count: 5, totalAmount: 500 },
{ status: EntryStatus.PAID, count: 15, totalAmount: 1500 },
]),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.getEntriesByStatus(mockTenantId);
expect(result).toHaveLength(3);
expect(result[0].status).toBe(EntryStatus.PENDING);
expect(result[0].count).toBe(10);
expect(result[0].totalAmount).toBe(1000);
expect(result[0].percentage).toBe(33); // 10/30 ≈ 33%
});
it('should handle empty results', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([]),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.getEntriesByStatus(mockTenantId);
expect(result).toHaveLength(0);
});
});
describe('getEntriesByScheme', () => {
it('should return entries grouped by scheme', async () => {
const mockQueryBuilder = {
innerJoin: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
addGroupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([
{ schemeId: 'scheme-001', schemeName: 'Standard', count: 20, totalAmount: 2000 },
{ schemeId: 'scheme-002', schemeName: 'Premium', count: 10, totalAmount: 1500 },
]),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.getEntriesByScheme(mockTenantId);
expect(result).toHaveLength(2);
expect(result[0].schemeId).toBe('scheme-001');
expect(result[0].schemeName).toBe('Standard');
expect(result[0].count).toBe(20);
expect(result[0].totalAmount).toBe(2000);
expect(result[0].percentage).toBe(67); // 20/30 ≈ 67%
});
it('should handle empty results', async () => {
const mockQueryBuilder = {
innerJoin: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
addGroupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([]),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.getEntriesByScheme(mockTenantId);
expect(result).toHaveLength(0);
});
});
describe('getEntriesByUser', () => {
it('should return entries grouped by user', async () => {
dataSource.query.mockResolvedValue([
{ userId: 'user-001', userName: 'John Doe', count: 15, totalAmount: 1500 },
{ userId: 'user-002', userName: 'Jane Smith', count: 10, totalAmount: 1000 },
{ userId: 'user-003', userName: 'Bob Wilson', count: 5, totalAmount: 500 },
]);
const result = await service.getEntriesByUser(mockTenantId);
expect(result).toHaveLength(3);
expect(result[0].userId).toBe('user-001');
expect(result[0].userName).toBe('John Doe');
expect(result[0].count).toBe(15);
expect(result[0].totalAmount).toBe(1500);
expect(result[0].percentage).toBe(50); // 15/30 = 50%
});
it('should handle empty results', async () => {
dataSource.query.mockResolvedValue([]);
const result = await service.getEntriesByUser(mockTenantId);
expect(result).toHaveLength(0);
});
});
describe('getTopEarners', () => {
it('should return top earners with default limit', async () => {
const allUsers = Array.from({ length: 15 }, (_, i) => ({
userId: `user-${i}`,
userName: `User ${i}`,
count: 15 - i,
totalAmount: (15 - i) * 100,
}));
dataSource.query.mockResolvedValue(allUsers);
const result = await service.getTopEarners(mockTenantId);
expect(result).toHaveLength(10); // default limit
expect(result[0].userId).toBe('user-0');
});
it('should return top earners with custom limit', async () => {
const allUsers = Array.from({ length: 15 }, (_, i) => ({
userId: `user-${i}`,
userName: `User ${i}`,
count: 15 - i,
totalAmount: (15 - i) * 100,
}));
dataSource.query.mockResolvedValue(allUsers);
const result = await service.getTopEarners(mockTenantId, 5);
expect(result).toHaveLength(5);
});
it('should handle fewer users than limit', async () => {
dataSource.query.mockResolvedValue([
{ userId: 'user-001', userName: 'John Doe', count: 5, totalAmount: 500 },
]);
const result = await service.getTopEarners(mockTenantId, 10);
expect(result).toHaveLength(1);
});
});
});

View File

@ -0,0 +1,455 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { NotFoundException, BadRequestException } from '@nestjs/common';
import { EntriesService } from '../services/entries.service';
import { CommissionEntryEntity, EntryStatus } from '../entities';
describe('EntriesService', () => {
let service: EntriesService;
let entryRepo: jest.Mocked<Repository<CommissionEntryEntity>>;
let dataSource: jest.Mocked<DataSource>;
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
const mockSchemeId = '550e8400-e29b-41d4-a716-446655440003';
const mockEntry: Partial<CommissionEntryEntity> = {
id: 'entry-001',
tenantId: mockTenantId,
userId: mockUserId,
schemeId: mockSchemeId,
assignmentId: null,
referenceType: 'sale',
referenceId: 'sale-001',
baseAmount: 1000,
rateApplied: 10,
commissionAmount: 100,
currency: 'USD',
status: EntryStatus.PENDING,
periodId: null,
paidAt: null,
paymentReference: null,
notes: null,
metadata: {},
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
approvedBy: null,
approvedAt: null,
};
beforeEach(async () => {
const mockEntryRepo = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
createQueryBuilder: jest.fn(),
};
const mockDataSource = {
query: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
EntriesService,
{ provide: getRepositoryToken(CommissionEntryEntity), useValue: mockEntryRepo },
{ provide: DataSource, useValue: mockDataSource },
],
}).compile();
service = module.get<EntriesService>(EntriesService);
entryRepo = module.get(getRepositoryToken(CommissionEntryEntity));
dataSource = module.get(DataSource);
});
afterEach(() => {
jest.resetAllMocks();
});
const createMockEntry = (overrides: Partial<CommissionEntryEntity> = {}): CommissionEntryEntity => ({
id: 'entry-001',
tenantId: mockTenantId,
userId: mockUserId,
schemeId: mockSchemeId,
assignmentId: null as any,
referenceType: 'sale',
referenceId: 'sale-001',
baseAmount: 1000,
rateApplied: 10,
commissionAmount: 100,
currency: 'USD',
status: EntryStatus.PENDING,
periodId: null as any,
paidAt: null as any,
paymentReference: null as any,
notes: null as any,
metadata: {},
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
approvedBy: null as any,
approvedAt: null as any,
scheme: null as any,
assignment: null as any,
period: null as any,
...overrides,
} as CommissionEntryEntity);
describe('create', () => {
it('should create an entry with calculated commission', async () => {
dataSource.query.mockResolvedValue([{ rate_applied: 10, commission_amount: 100 }]);
entryRepo.create.mockReturnValue(mockEntry as CommissionEntryEntity);
entryRepo.save.mockResolvedValue(mockEntry as CommissionEntryEntity);
const dto = {
userId: mockUserId,
schemeId: mockSchemeId,
referenceType: 'sale',
referenceId: 'sale-001',
baseAmount: 1000,
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.baseAmount).toBe(1000);
expect(result.rateApplied).toBe(10);
expect(result.commissionAmount).toBe(100);
expect(dataSource.query).toHaveBeenCalledWith(
`SELECT * FROM commissions.calculate_commission($1, $2, $3, $4)`,
[dto.schemeId, dto.userId, dto.baseAmount, mockTenantId],
);
});
it('should throw BadRequestException when commission is zero', async () => {
dataSource.query.mockResolvedValue([{ rate_applied: 0, commission_amount: 0 }]);
const dto = {
userId: mockUserId,
schemeId: mockSchemeId,
referenceType: 'sale',
referenceId: 'sale-001',
baseAmount: 1000,
};
await expect(service.create(mockTenantId, mockUserId, dto)).rejects.toThrow(
BadRequestException,
);
});
it('should create an entry with metadata', async () => {
const entryWithMetadata = {
...mockEntry,
metadata: { source: 'web', campaign: 'summer2026' },
};
dataSource.query.mockResolvedValue([{ rate_applied: 10, commission_amount: 100 }]);
entryRepo.create.mockReturnValue(entryWithMetadata as CommissionEntryEntity);
entryRepo.save.mockResolvedValue(entryWithMetadata as CommissionEntryEntity);
const dto = {
userId: mockUserId,
schemeId: mockSchemeId,
referenceType: 'sale',
referenceId: 'sale-001',
baseAmount: 1000,
metadata: { source: 'web', campaign: 'summer2026' },
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.metadata).toEqual({ source: 'web', campaign: 'summer2026' });
});
});
describe('findAll', () => {
it('should return paginated entries', async () => {
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockEntry], 1]),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.findAll(mockTenantId, { page: 1, limit: 10 });
expect(result.items).toHaveLength(1);
expect(result.total).toBe(1);
});
it('should filter by status', async () => {
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockEntry], 1]),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
await service.findAll(mockTenantId, { status: EntryStatus.PENDING });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'e.status = :status',
{ status: EntryStatus.PENDING },
);
});
it('should filter by userId', async () => {
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockEntry], 1]),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
await service.findAll(mockTenantId, { userId: mockUserId });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'e.user_id = :userId',
{ userId: mockUserId },
);
});
it('should filter by referenceType', async () => {
const mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockEntry], 1]),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
await service.findAll(mockTenantId, { referenceType: 'sale' });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'e.reference_type = :referenceType',
{ referenceType: 'sale' },
);
});
});
describe('findOne', () => {
it('should return an entry by id', async () => {
entryRepo.findOne.mockResolvedValue(mockEntry as CommissionEntryEntity);
const result = await service.findOne(mockTenantId, 'entry-001');
expect(result.id).toBe('entry-001');
expect(result.commissionAmount).toBe(100);
});
it('should throw NotFoundException if entry not found', async () => {
entryRepo.findOne.mockResolvedValue(null);
await expect(service.findOne(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
describe('update', () => {
it('should update an entry', async () => {
const updatedEntry = { ...mockEntry, notes: 'Updated notes' };
entryRepo.findOne.mockResolvedValue(mockEntry as CommissionEntryEntity);
entryRepo.save.mockResolvedValue(updatedEntry as CommissionEntryEntity);
const result = await service.update(mockTenantId, 'entry-001', { notes: 'Updated notes' });
expect(result.notes).toBe('Updated notes');
});
it('should throw NotFoundException if entry not found', async () => {
entryRepo.findOne.mockResolvedValue(null);
await expect(
service.update(mockTenantId, 'invalid-id', { notes: 'Test' }),
).rejects.toThrow(NotFoundException);
});
});
describe('approve', () => {
it('should approve a pending entry', async () => {
const approvedEntry = {
...mockEntry,
status: EntryStatus.APPROVED,
approvedBy: mockUserId,
approvedAt: new Date(),
};
entryRepo.findOne.mockResolvedValue(mockEntry as CommissionEntryEntity);
entryRepo.save.mockResolvedValue(approvedEntry as CommissionEntryEntity);
const result = await service.approve(mockTenantId, 'entry-001', mockUserId, {});
expect(result.status).toBe(EntryStatus.APPROVED);
expect(result.approvedBy).toBe(mockUserId);
expect(result.approvedAt).toBeDefined();
});
it('should throw BadRequestException if entry is not pending', async () => {
const approvedEntry = { ...mockEntry, status: EntryStatus.APPROVED };
entryRepo.findOne.mockResolvedValue(approvedEntry as CommissionEntryEntity);
await expect(
service.approve(mockTenantId, 'entry-001', mockUserId, {}),
).rejects.toThrow(BadRequestException);
});
it('should throw NotFoundException if entry not found', async () => {
entryRepo.findOne.mockResolvedValue(null);
await expect(
service.approve(mockTenantId, 'invalid-id', mockUserId, {}),
).rejects.toThrow(NotFoundException);
});
});
describe('reject', () => {
it('should reject a pending entry', async () => {
const pendingEntry = createMockEntry({ status: EntryStatus.PENDING });
const rejectedEntry = createMockEntry({
status: EntryStatus.REJECTED,
approvedBy: mockUserId,
approvedAt: new Date(),
notes: 'Invalid sale',
});
entryRepo.findOne.mockResolvedValue(pendingEntry);
entryRepo.save.mockResolvedValue(rejectedEntry);
const result = await service.reject(mockTenantId, 'entry-001', mockUserId, {
notes: 'Invalid sale',
});
expect(result.status).toBe(EntryStatus.REJECTED);
expect(result.notes).toBe('Invalid sale');
});
it('should throw BadRequestException if entry is not pending', async () => {
const approvedEntry = createMockEntry({ status: EntryStatus.APPROVED });
entryRepo.findOne.mockResolvedValue(approvedEntry);
await expect(
service.reject(mockTenantId, 'entry-001', mockUserId, { notes: 'Reason' }),
).rejects.toThrow(BadRequestException);
});
});
describe('calculateCommission', () => {
it('should calculate commission without creating entry', async () => {
dataSource.query.mockResolvedValue([{ rate_applied: 10, commission_amount: 100 }]);
const result = await service.calculateCommission(mockTenantId, {
schemeId: mockSchemeId,
userId: mockUserId,
amount: 1000,
});
expect(result.rateApplied).toBe(10);
expect(result.commissionAmount).toBe(100);
});
it('should return zero for invalid calculation', async () => {
dataSource.query.mockResolvedValue([]);
const result = await service.calculateCommission(mockTenantId, {
schemeId: mockSchemeId,
userId: mockUserId,
amount: 1000,
});
expect(result.rateApplied).toBe(0);
expect(result.commissionAmount).toBe(0);
});
});
describe('markAsPaid', () => {
it('should mark an approved entry as paid', async () => {
const approvedEntry = { ...mockEntry, status: EntryStatus.APPROVED };
const paidEntry = {
...approvedEntry,
status: EntryStatus.PAID,
paidAt: new Date(),
paymentReference: 'PAY-001',
};
entryRepo.findOne.mockResolvedValue(approvedEntry as CommissionEntryEntity);
entryRepo.save.mockResolvedValue(paidEntry as CommissionEntryEntity);
const result = await service.markAsPaid(mockTenantId, 'entry-001', 'PAY-001');
expect(result.status).toBe(EntryStatus.PAID);
expect(result.paymentReference).toBe('PAY-001');
expect(result.paidAt).toBeDefined();
});
it('should throw BadRequestException if entry is not approved', async () => {
entryRepo.findOne.mockResolvedValue(mockEntry as CommissionEntryEntity);
await expect(
service.markAsPaid(mockTenantId, 'entry-001', 'PAY-001'),
).rejects.toThrow(BadRequestException);
});
it('should throw NotFoundException if entry not found', async () => {
entryRepo.findOne.mockResolvedValue(null);
await expect(
service.markAsPaid(mockTenantId, 'invalid-id', 'PAY-001'),
).rejects.toThrow(NotFoundException);
});
});
describe('bulkApprove', () => {
it('should bulk approve multiple entries', async () => {
const mockQueryBuilder = {
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
execute: jest.fn().mockResolvedValue({ affected: 3 }),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.bulkApprove(
mockTenantId,
['entry-001', 'entry-002', 'entry-003'],
mockUserId,
);
expect(result).toBe(3);
});
it('should return zero when no entries match', async () => {
const mockQueryBuilder = {
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
execute: jest.fn().mockResolvedValue({ affected: 0 }),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.bulkApprove(mockTenantId, ['invalid-id'], mockUserId);
expect(result).toBe(0);
});
});
});

View File

@ -0,0 +1,382 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { NotFoundException, BadRequestException } from '@nestjs/common';
import { PeriodsService } from '../services/periods.service';
import { CommissionPeriodEntity, PeriodStatus, CommissionEntryEntity, EntryStatus } from '../entities';
describe('PeriodsService', () => {
let service: PeriodsService;
let periodRepo: jest.Mocked<Repository<CommissionPeriodEntity>>;
let entryRepo: jest.Mocked<Repository<CommissionEntryEntity>>;
let dataSource: jest.Mocked<DataSource>;
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
const mockPeriod: Partial<CommissionPeriodEntity> = {
id: 'period-001',
tenantId: mockTenantId,
name: 'January 2026',
startsAt: new Date('2026-01-01'),
endsAt: new Date('2026-01-31'),
totalEntries: 0,
totalAmount: 0,
currency: 'USD',
status: PeriodStatus.OPEN,
closedAt: null,
closedBy: null,
paidAt: null,
paidBy: null,
paymentReference: null,
paymentNotes: null,
createdAt: new Date('2026-01-01'),
createdBy: mockUserId,
};
beforeEach(async () => {
const mockPeriodRepo = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
remove: jest.fn(),
createQueryBuilder: jest.fn(),
};
const mockEntryRepo = {
count: jest.fn(),
createQueryBuilder: jest.fn(),
};
const mockDataSource = {
query: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
PeriodsService,
{ provide: getRepositoryToken(CommissionPeriodEntity), useValue: mockPeriodRepo },
{ provide: getRepositoryToken(CommissionEntryEntity), useValue: mockEntryRepo },
{ provide: DataSource, useValue: mockDataSource },
],
}).compile();
service = module.get<PeriodsService>(PeriodsService);
periodRepo = module.get(getRepositoryToken(CommissionPeriodEntity));
entryRepo = module.get(getRepositoryToken(CommissionEntryEntity));
dataSource = module.get(DataSource);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
it('should create a period successfully', async () => {
periodRepo.create.mockReturnValue(mockPeriod as CommissionPeriodEntity);
periodRepo.save.mockResolvedValue(mockPeriod as CommissionPeriodEntity);
const dto = {
name: 'January 2026',
startsAt: '2026-01-01',
endsAt: '2026-01-31',
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.name).toBe('January 2026');
expect(result.status).toBe(PeriodStatus.OPEN);
expect(periodRepo.create).toHaveBeenCalled();
expect(periodRepo.save).toHaveBeenCalled();
});
it('should create a period with custom currency', async () => {
const eurPeriod = { ...mockPeriod, currency: 'EUR' };
periodRepo.create.mockReturnValue(eurPeriod as CommissionPeriodEntity);
periodRepo.save.mockResolvedValue(eurPeriod as CommissionPeriodEntity);
const dto = {
name: 'January 2026',
startsAt: '2026-01-01',
endsAt: '2026-01-31',
currency: 'EUR',
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.currency).toBe('EUR');
});
});
describe('findAll', () => {
it('should return paginated periods', async () => {
const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockPeriod], 1]),
};
periodRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.findAll(mockTenantId, { page: 1, limit: 10 });
expect(result.items).toHaveLength(1);
expect(result.total).toBe(1);
});
it('should filter by status', async () => {
const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockPeriod], 1]),
};
periodRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
await service.findAll(mockTenantId, { status: PeriodStatus.OPEN });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'p.status = :status',
{ status: PeriodStatus.OPEN },
);
});
});
describe('findOne', () => {
it('should return a period by id', async () => {
periodRepo.findOne.mockResolvedValue(mockPeriod as CommissionPeriodEntity);
const result = await service.findOne(mockTenantId, 'period-001');
expect(result.id).toBe('period-001');
expect(result.name).toBe('January 2026');
});
it('should throw NotFoundException if period not found', async () => {
periodRepo.findOne.mockResolvedValue(null);
await expect(service.findOne(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
describe('update', () => {
it('should update an open period', async () => {
const updatedPeriod = { ...mockPeriod, name: 'Updated Period' };
periodRepo.findOne.mockResolvedValue(mockPeriod as CommissionPeriodEntity);
periodRepo.save.mockResolvedValue(updatedPeriod as CommissionPeriodEntity);
const result = await service.update(mockTenantId, 'period-001', { name: 'Updated Period' });
expect(result.name).toBe('Updated Period');
});
it('should throw BadRequestException for non-open period', async () => {
const closedPeriod = { ...mockPeriod, status: PeriodStatus.CLOSED };
periodRepo.findOne.mockResolvedValue(closedPeriod as CommissionPeriodEntity);
await expect(
service.update(mockTenantId, 'period-001', { name: 'Test' }),
).rejects.toThrow(BadRequestException);
});
it('should throw NotFoundException if period not found', async () => {
periodRepo.findOne.mockResolvedValue(null);
await expect(
service.update(mockTenantId, 'invalid-id', { name: 'Test' }),
).rejects.toThrow(NotFoundException);
});
});
describe('close', () => {
it('should close an open period', async () => {
const closedPeriod = {
...mockPeriod,
status: PeriodStatus.CLOSED,
closedAt: new Date(),
closedBy: mockUserId,
};
periodRepo.findOne
.mockResolvedValueOnce(mockPeriod as CommissionPeriodEntity)
.mockResolvedValueOnce(closedPeriod as CommissionPeriodEntity);
dataSource.query.mockResolvedValue([]);
const result = await service.close(mockTenantId, 'period-001', mockUserId, {});
expect(result.status).toBe(PeriodStatus.CLOSED);
expect(dataSource.query).toHaveBeenCalledWith(
`SELECT commissions.close_period($1, $2)`,
['period-001', mockUserId],
);
});
it('should throw BadRequestException for non-open period', async () => {
const closedPeriod = { ...mockPeriod, status: PeriodStatus.CLOSED };
periodRepo.findOne.mockResolvedValue(closedPeriod as CommissionPeriodEntity);
await expect(
service.close(mockTenantId, 'period-001', mockUserId, {}),
).rejects.toThrow(BadRequestException);
});
it('should throw NotFoundException if period not found', async () => {
periodRepo.findOne.mockResolvedValue(null);
await expect(
service.close(mockTenantId, 'invalid-id', mockUserId, {}),
).rejects.toThrow(NotFoundException);
});
});
describe('markAsPaid', () => {
it('should mark a closed period as paid', async () => {
const closedPeriod = { ...mockPeriod, status: PeriodStatus.CLOSED };
const paidPeriod = {
...closedPeriod,
status: PeriodStatus.PAID,
paidAt: new Date(),
paidBy: mockUserId,
paymentReference: 'PAY-001',
};
const mockQueryBuilder = {
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
execute: jest.fn().mockResolvedValue({ affected: 5 }),
};
periodRepo.findOne.mockResolvedValue(closedPeriod as CommissionPeriodEntity);
periodRepo.save.mockResolvedValue(paidPeriod as CommissionPeriodEntity);
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.markAsPaid(mockTenantId, 'period-001', mockUserId, {
paymentReference: 'PAY-001',
});
expect(result.status).toBe(PeriodStatus.PAID);
expect(result.paymentReference).toBe('PAY-001');
});
it('should throw BadRequestException for open period', async () => {
periodRepo.findOne.mockResolvedValue(mockPeriod as CommissionPeriodEntity);
await expect(
service.markAsPaid(mockTenantId, 'period-001', mockUserId, {}),
).rejects.toThrow(BadRequestException);
});
it('should throw NotFoundException if period not found', async () => {
periodRepo.findOne.mockResolvedValue(null);
await expect(
service.markAsPaid(mockTenantId, 'invalid-id', mockUserId, {}),
).rejects.toThrow(NotFoundException);
});
});
describe('remove', () => {
it('should delete an open period without entries', async () => {
periodRepo.findOne.mockResolvedValue(mockPeriod as CommissionPeriodEntity);
entryRepo.count.mockResolvedValue(0);
await service.remove(mockTenantId, 'period-001');
expect(periodRepo.remove).toHaveBeenCalledWith(mockPeriod);
});
it('should throw BadRequestException for non-open period', async () => {
const closedPeriod = { ...mockPeriod, status: PeriodStatus.CLOSED };
periodRepo.findOne.mockResolvedValue(closedPeriod as CommissionPeriodEntity);
await expect(service.remove(mockTenantId, 'period-001')).rejects.toThrow(
BadRequestException,
);
});
it('should throw BadRequestException if period has entries', async () => {
periodRepo.findOne.mockResolvedValue(mockPeriod as CommissionPeriodEntity);
entryRepo.count.mockResolvedValue(5);
await expect(service.remove(mockTenantId, 'period-001')).rejects.toThrow(
BadRequestException,
);
});
it('should throw NotFoundException if period not found', async () => {
periodRepo.findOne.mockResolvedValue(null);
await expect(service.remove(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
describe('getCurrentPeriod', () => {
it('should return the current open period', async () => {
periodRepo.findOne.mockResolvedValue(mockPeriod as CommissionPeriodEntity);
const result = await service.getCurrentPeriod(mockTenantId);
expect(result).not.toBeNull();
expect(result!.status).toBe(PeriodStatus.OPEN);
});
it('should return null if no open period', async () => {
periodRepo.findOne.mockResolvedValue(null);
const result = await service.getCurrentPeriod(mockTenantId);
expect(result).toBeNull();
});
});
describe('assignEntriesToPeriod', () => {
it('should assign entries to a period', async () => {
const mockQueryBuilder = {
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
execute: jest.fn().mockResolvedValue({ affected: 3 }),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.assignEntriesToPeriod(
mockTenantId,
'period-001',
['entry-001', 'entry-002', 'entry-003'],
);
expect(result).toBe(3);
});
it('should return zero when no entries match', async () => {
const mockQueryBuilder = {
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
execute: jest.fn().mockResolvedValue({ affected: 0 }),
};
entryRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.assignEntriesToPeriod(mockTenantId, 'period-001', []);
expect(result).toBe(0);
});
});
});

View File

@ -0,0 +1,342 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotFoundException } from '@nestjs/common';
import { SchemesService } from '../services/schemes.service';
import { CommissionSchemeEntity, SchemeType, AppliesTo } from '../entities';
describe('SchemesService', () => {
let service: SchemesService;
let schemeRepo: jest.Mocked<Repository<CommissionSchemeEntity>>;
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
const mockScheme: Partial<CommissionSchemeEntity> = {
id: 'scheme-001',
tenantId: mockTenantId,
name: 'Standard Commission',
description: 'Standard percentage commission',
type: SchemeType.PERCENTAGE,
rate: 10,
fixedAmount: 0,
tiers: [],
appliesTo: AppliesTo.ALL,
productIds: [],
categoryIds: [],
minAmount: 0,
maxAmount: null,
isActive: true,
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
createdBy: mockUserId,
deletedAt: null,
};
beforeEach(async () => {
const mockSchemeRepo = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
createQueryBuilder: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
SchemesService,
{ provide: getRepositoryToken(CommissionSchemeEntity), useValue: mockSchemeRepo },
],
}).compile();
service = module.get<SchemesService>(SchemesService);
schemeRepo = module.get(getRepositoryToken(CommissionSchemeEntity));
});
afterEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
it('should create a percentage scheme successfully', async () => {
schemeRepo.create.mockReturnValue(mockScheme as CommissionSchemeEntity);
schemeRepo.save.mockResolvedValue(mockScheme as CommissionSchemeEntity);
const dto = {
name: 'Standard Commission',
description: 'Standard percentage commission',
type: SchemeType.PERCENTAGE,
rate: 10,
appliesTo: AppliesTo.ALL,
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.name).toBe('Standard Commission');
expect(result.type).toBe(SchemeType.PERCENTAGE);
expect(result.rate).toBe(10);
expect(schemeRepo.create).toHaveBeenCalled();
expect(schemeRepo.save).toHaveBeenCalled();
});
it('should create a fixed amount scheme', async () => {
const fixedScheme = { ...mockScheme, type: SchemeType.FIXED, fixedAmount: 50 };
schemeRepo.create.mockReturnValue(fixedScheme as CommissionSchemeEntity);
schemeRepo.save.mockResolvedValue(fixedScheme as CommissionSchemeEntity);
const dto = {
name: 'Fixed Commission',
type: SchemeType.FIXED,
fixedAmount: 50,
appliesTo: AppliesTo.ALL,
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.type).toBe(SchemeType.FIXED);
expect(result.fixedAmount).toBe(50);
});
it('should create a tiered scheme', async () => {
const tiers = [
{ from: 0, to: 1000, rate: 5 },
{ from: 1000, to: 5000, rate: 10 },
{ from: 5000, to: null, rate: 15 },
];
const tieredScheme = { ...mockScheme, type: SchemeType.TIERED, tiers };
schemeRepo.create.mockReturnValue(tieredScheme as CommissionSchemeEntity);
schemeRepo.save.mockResolvedValue(tieredScheme as CommissionSchemeEntity);
const dto = {
name: 'Tiered Commission',
type: SchemeType.TIERED,
tiers,
appliesTo: AppliesTo.ALL,
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.type).toBe(SchemeType.TIERED);
expect(result.tiers).toHaveLength(3);
});
it('should create a scheme with product restrictions', async () => {
const productScheme = {
...mockScheme,
appliesTo: AppliesTo.PRODUCTS,
productIds: ['product-001', 'product-002'],
};
schemeRepo.create.mockReturnValue(productScheme as CommissionSchemeEntity);
schemeRepo.save.mockResolvedValue(productScheme as CommissionSchemeEntity);
const dto = {
name: 'Product Commission',
type: SchemeType.PERCENTAGE,
rate: 15,
appliesTo: AppliesTo.PRODUCTS,
productIds: ['product-001', 'product-002'],
};
const result = await service.create(mockTenantId, mockUserId, dto);
expect(result.appliesTo).toBe(AppliesTo.PRODUCTS);
expect(result.productIds).toHaveLength(2);
});
});
describe('findAll', () => {
it('should return paginated schemes', async () => {
const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockScheme], 1]),
};
schemeRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.findAll(mockTenantId, { page: 1, limit: 10 });
expect(result.items).toHaveLength(1);
expect(result.total).toBe(1);
expect(result.page).toBe(1);
expect(result.limit).toBe(10);
expect(result.totalPages).toBe(1);
});
it('should filter by type', async () => {
const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockScheme], 1]),
};
schemeRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
await service.findAll(mockTenantId, { type: SchemeType.PERCENTAGE });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
's.type = :type',
{ type: SchemeType.PERCENTAGE },
);
});
it('should filter by isActive', async () => {
const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
};
schemeRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
await service.findAll(mockTenantId, { isActive: true });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
's.is_active = :isActive',
{ isActive: true },
);
});
it('should search by name or description', async () => {
const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[mockScheme], 1]),
};
schemeRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
await service.findAll(mockTenantId, { search: 'Standard' });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
'(s.name ILIKE :search OR s.description ILIKE :search)',
{ search: '%Standard%' },
);
});
});
describe('findOne', () => {
it('should return a scheme by id', async () => {
schemeRepo.findOne.mockResolvedValue(mockScheme as CommissionSchemeEntity);
const result = await service.findOne(mockTenantId, 'scheme-001');
expect(result.id).toBe('scheme-001');
expect(result.name).toBe('Standard Commission');
});
it('should throw NotFoundException if scheme not found', async () => {
schemeRepo.findOne.mockResolvedValue(null);
await expect(service.findOne(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
describe('update', () => {
it('should update a scheme successfully', async () => {
const updatedScheme = { ...mockScheme, name: 'Updated Commission', rate: 15 };
schemeRepo.findOne.mockResolvedValue(mockScheme as CommissionSchemeEntity);
schemeRepo.save.mockResolvedValue(updatedScheme as CommissionSchemeEntity);
const result = await service.update(mockTenantId, 'scheme-001', {
name: 'Updated Commission',
rate: 15,
});
expect(result.name).toBe('Updated Commission');
expect(result.rate).toBe(15);
});
it('should throw NotFoundException if scheme not found', async () => {
schemeRepo.findOne.mockResolvedValue(null);
await expect(
service.update(mockTenantId, 'invalid-id', { name: 'Test' }),
).rejects.toThrow(NotFoundException);
});
});
describe('remove', () => {
it('should soft delete a scheme', async () => {
schemeRepo.findOne.mockResolvedValue(mockScheme as CommissionSchemeEntity);
schemeRepo.save.mockResolvedValue({
...mockScheme,
deletedAt: new Date(),
} as CommissionSchemeEntity);
await service.remove(mockTenantId, 'scheme-001');
expect(schemeRepo.save).toHaveBeenCalledWith(
expect.objectContaining({ deletedAt: expect.any(Date) }),
);
});
it('should throw NotFoundException if scheme not found', async () => {
schemeRepo.findOne.mockResolvedValue(null);
await expect(service.remove(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
describe('activate', () => {
it('should activate a scheme', async () => {
const inactiveScheme = { ...mockScheme, isActive: false };
schemeRepo.findOne.mockResolvedValue(inactiveScheme as CommissionSchemeEntity);
schemeRepo.save.mockResolvedValue({
...inactiveScheme,
isActive: true,
} as CommissionSchemeEntity);
const result = await service.activate(mockTenantId, 'scheme-001');
expect(result.isActive).toBe(true);
});
it('should throw NotFoundException if scheme not found', async () => {
schemeRepo.findOne.mockResolvedValue(null);
await expect(service.activate(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
describe('deactivate', () => {
it('should deactivate a scheme', async () => {
schemeRepo.findOne.mockResolvedValue(mockScheme as CommissionSchemeEntity);
schemeRepo.save.mockResolvedValue({
...mockScheme,
isActive: false,
} as CommissionSchemeEntity);
const result = await service.deactivate(mockTenantId, 'scheme-001');
expect(result.isActive).toBe(false);
});
it('should throw NotFoundException if scheme not found', async () => {
schemeRepo.findOne.mockResolvedValue(null);
await expect(service.deactivate(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
});

View File

@ -0,0 +1,342 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { NotFoundException } from '@nestjs/common';
import { PipelineService } from '../services/pipeline.service';
import { PipelineStageEntity, OpportunityEntity } from '../entities';
describe('PipelineService', () => {
let service: PipelineService;
let stageRepo: jest.Mocked<Repository<PipelineStageEntity>>;
let opportunityRepo: jest.Mocked<Repository<OpportunityEntity>>;
let dataSource: jest.Mocked<DataSource>;
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
const mockStage: Partial<PipelineStageEntity> = {
id: 'stage-001',
tenantId: mockTenantId,
name: 'Qualification',
position: 1,
color: '#3B82F6',
isWon: false,
isLost: false,
isActive: true,
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
};
const mockStages: Partial<PipelineStageEntity>[] = [
mockStage,
{
id: 'stage-002',
tenantId: mockTenantId,
name: 'Proposal',
position: 2,
color: '#F59E0B',
isWon: false,
isLost: false,
isActive: true,
},
{
id: 'stage-003',
tenantId: mockTenantId,
name: 'Closed Won',
position: 3,
color: '#10B981',
isWon: true,
isLost: false,
isActive: true,
},
];
beforeEach(async () => {
const mockStageRepo = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
remove: jest.fn(),
createQueryBuilder: jest.fn(),
};
const mockOpportunityRepo = {
count: jest.fn(),
createQueryBuilder: jest.fn(),
};
const mockDataSource = {
query: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
PipelineService,
{ provide: getRepositoryToken(PipelineStageEntity), useValue: mockStageRepo },
{ provide: getRepositoryToken(OpportunityEntity), useValue: mockOpportunityRepo },
{ provide: DataSource, useValue: mockDataSource },
],
}).compile();
service = module.get<PipelineService>(PipelineService);
stageRepo = module.get(getRepositoryToken(PipelineStageEntity));
opportunityRepo = module.get(getRepositoryToken(OpportunityEntity));
dataSource = module.get(DataSource);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('initializeDefaults', () => {
it('should call database function to initialize default stages', async () => {
dataSource.query.mockResolvedValue([]);
await service.initializeDefaults(mockTenantId);
expect(dataSource.query).toHaveBeenCalledWith(
`SELECT sales.initialize_default_stages($1)`,
[mockTenantId],
);
});
});
describe('findAll', () => {
it('should return all stages with opportunity stats', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([
{ stageId: 'stage-001', count: 5, total: 50000 },
{ stageId: 'stage-002', count: 3, total: 30000 },
]),
};
stageRepo.find.mockResolvedValue(mockStages as PipelineStageEntity[]);
opportunityRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.findAll(mockTenantId);
expect(result).toHaveLength(3);
expect(result[0].opportunityCount).toBe(5);
expect(result[0].totalAmount).toBe(50000);
expect(result[2].opportunityCount).toBe(0);
expect(stageRepo.find).toHaveBeenCalledWith({
where: { tenantId: mockTenantId },
order: { position: 'ASC' },
});
});
it('should return stages with zero stats when no opportunities', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([]),
};
stageRepo.find.mockResolvedValue([mockStage] as PipelineStageEntity[]);
opportunityRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.findAll(mockTenantId);
expect(result).toHaveLength(1);
expect(result[0].opportunityCount).toBe(0);
expect(result[0].totalAmount).toBe(0);
});
});
describe('findOne', () => {
it('should return a stage by id', async () => {
stageRepo.findOne.mockResolvedValue(mockStage as PipelineStageEntity);
const result = await service.findOne(mockTenantId, 'stage-001');
expect(result.id).toBe('stage-001');
expect(result.name).toBe('Qualification');
expect(stageRepo.findOne).toHaveBeenCalledWith({
where: { id: 'stage-001', tenantId: mockTenantId },
});
});
it('should throw NotFoundException if stage not found', async () => {
stageRepo.findOne.mockResolvedValue(null);
await expect(service.findOne(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
describe('create', () => {
it('should create a new stage at the end', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getRawOne: jest.fn().mockResolvedValue({ max: 2 }),
};
stageRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
stageRepo.create.mockReturnValue(mockStage as PipelineStageEntity);
stageRepo.save.mockResolvedValue(mockStage as PipelineStageEntity);
const dto = {
name: 'Qualification',
color: '#3B82F6',
};
const result = await service.create(mockTenantId, dto);
expect(result.name).toBe('Qualification');
expect(stageRepo.create).toHaveBeenCalled();
expect(stageRepo.save).toHaveBeenCalled();
});
it('should create a stage with custom position', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getRawOne: jest.fn().mockResolvedValue({ max: 5 }),
};
stageRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
stageRepo.create.mockReturnValue({ ...mockStage, position: 3 } as PipelineStageEntity);
stageRepo.save.mockResolvedValue({ ...mockStage, position: 3 } as PipelineStageEntity);
const dto = {
name: 'Custom Stage',
position: 3,
};
const result = await service.create(mockTenantId, dto);
expect(stageRepo.create).toHaveBeenCalledWith(
expect.objectContaining({ position: 3 }),
);
});
it('should create a won stage', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getRawOne: jest.fn().mockResolvedValue({ max: 2 }),
};
const wonStage = { ...mockStage, isWon: true };
stageRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
stageRepo.create.mockReturnValue(wonStage as PipelineStageEntity);
stageRepo.save.mockResolvedValue(wonStage as PipelineStageEntity);
const dto = {
name: 'Closed Won',
isWon: true,
};
const result = await service.create(mockTenantId, dto);
expect(result.isWon).toBe(true);
});
});
describe('update', () => {
it('should update a stage successfully', async () => {
const updatedStage = { ...mockStage, name: 'Updated Stage' };
stageRepo.findOne.mockResolvedValue(mockStage as PipelineStageEntity);
stageRepo.save.mockResolvedValue(updatedStage as PipelineStageEntity);
const result = await service.update(mockTenantId, 'stage-001', { name: 'Updated Stage' });
expect(result.name).toBe('Updated Stage');
expect(stageRepo.save).toHaveBeenCalled();
});
it('should throw NotFoundException if stage not found', async () => {
stageRepo.findOne.mockResolvedValue(null);
await expect(
service.update(mockTenantId, 'invalid-id', { name: 'Test' }),
).rejects.toThrow(NotFoundException);
});
});
describe('remove', () => {
it('should delete a stage without opportunities', async () => {
stageRepo.findOne.mockResolvedValue(mockStage as PipelineStageEntity);
opportunityRepo.count.mockResolvedValue(0);
await service.remove(mockTenantId, 'stage-001');
expect(stageRepo.remove).toHaveBeenCalledWith(mockStage);
});
it('should throw error when stage has opportunities', async () => {
stageRepo.findOne.mockResolvedValue(mockStage as PipelineStageEntity);
opportunityRepo.count.mockResolvedValue(5);
await expect(service.remove(mockTenantId, 'stage-001')).rejects.toThrow(
NotFoundException,
);
});
it('should throw NotFoundException if stage not found', async () => {
stageRepo.findOne.mockResolvedValue(null);
await expect(service.remove(mockTenantId, 'invalid-id')).rejects.toThrow(
NotFoundException,
);
});
});
describe('reorder', () => {
it('should reorder stages successfully', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([]),
};
stageRepo.find.mockResolvedValue(mockStages as PipelineStageEntity[]);
stageRepo.save.mockResolvedValue(mockStages as PipelineStageEntity[]);
opportunityRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const dto = {
stageIds: ['stage-003', 'stage-001', 'stage-002'],
};
const result = await service.reorder(mockTenantId, dto);
expect(stageRepo.save).toHaveBeenCalled();
expect(result).toBeDefined();
});
it('should handle empty stage list', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([]),
};
stageRepo.find.mockResolvedValue([]);
stageRepo.save.mockResolvedValue([]);
opportunityRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const dto = { stageIds: [] };
const result = await service.reorder(mockTenantId, dto);
expect(result).toHaveLength(0);
});
});
});

View File

@ -0,0 +1,308 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { SalesDashboardService } from '../services/sales-dashboard.service';
import { LeadEntity, OpportunityEntity, ActivityEntity, OpportunityStage, LeadStatus, ActivityStatus } from '../entities';
describe('SalesDashboardService', () => {
let service: SalesDashboardService;
let leadRepo: jest.Mocked<Repository<LeadEntity>>;
let opportunityRepo: jest.Mocked<Repository<OpportunityEntity>>;
let activityRepo: jest.Mocked<Repository<ActivityEntity>>;
let dataSource: jest.Mocked<DataSource>;
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
beforeEach(async () => {
const mockLeadRepo = {
count: jest.fn(),
createQueryBuilder: jest.fn(),
};
const mockOpportunityRepo = {
count: jest.fn(),
createQueryBuilder: jest.fn(),
};
const mockActivityRepo = {
count: jest.fn(),
};
const mockDataSource = {
query: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
SalesDashboardService,
{ provide: getRepositoryToken(LeadEntity), useValue: mockLeadRepo },
{ provide: getRepositoryToken(OpportunityEntity), useValue: mockOpportunityRepo },
{ provide: getRepositoryToken(ActivityEntity), useValue: mockActivityRepo },
{ provide: DataSource, useValue: mockDataSource },
],
}).compile();
service = module.get<SalesDashboardService>(SalesDashboardService);
leadRepo = module.get(getRepositoryToken(LeadEntity));
opportunityRepo = module.get(getRepositoryToken(OpportunityEntity));
activityRepo = module.get(getRepositoryToken(ActivityEntity));
dataSource = module.get(DataSource);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getDashboardSummary', () => {
it('should return dashboard summary with all stats', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
setParameter: jest.fn().mockReturnThis(),
getRawOne: jest.fn().mockResolvedValue({
total: 10,
totalValue: 100000,
wonCount: 3,
wonValue: 45000,
}),
};
leadRepo.count
.mockResolvedValueOnce(50) // total leads
.mockResolvedValueOnce(10); // converted leads
opportunityRepo.count.mockResolvedValue(20);
opportunityRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
activityRepo.count.mockResolvedValue(25);
const result = await service.getDashboardSummary(mockTenantId);
expect(result.totalLeads).toBe(50);
expect(result.totalOpportunities).toBe(20);
expect(result.totalPipelineValue).toBe(100000);
expect(result.wonDeals).toBe(3);
expect(result.conversionRate).toBe(20);
expect(result.averageDealSize).toBe(15000);
expect(result.activitiesThisWeek).toBe(25);
expect(result.currency).toBe('USD');
});
it('should handle zero leads gracefully', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
setParameter: jest.fn().mockReturnThis(),
getRawOne: jest.fn().mockResolvedValue({
total: 0,
totalValue: 0,
wonCount: 0,
wonValue: 0,
}),
};
leadRepo.count.mockResolvedValue(0);
opportunityRepo.count.mockResolvedValue(0);
opportunityRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
activityRepo.count.mockResolvedValue(0);
const result = await service.getDashboardSummary(mockTenantId);
expect(result.totalLeads).toBe(0);
expect(result.conversionRate).toBe(0);
expect(result.averageDealSize).toBe(0);
});
});
describe('getLeadsByStatus', () => {
it('should return leads grouped by status', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([
{ status: LeadStatus.NEW, count: 20 },
{ status: LeadStatus.CONTACTED, count: 15 },
{ status: LeadStatus.QUALIFIED, count: 10 },
{ status: LeadStatus.CONVERTED, count: 5 },
]),
};
leadRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.getLeadsByStatus(mockTenantId);
expect(result).toHaveLength(4);
expect(result[0].status).toBe(LeadStatus.NEW);
expect(result[0].count).toBe(20);
expect(result[0].percentage).toBe(40); // 20/50 = 40%
});
it('should handle empty results', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([]),
};
leadRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.getLeadsByStatus(mockTenantId);
expect(result).toHaveLength(0);
});
});
describe('getLeadsBySource', () => {
it('should return leads grouped by source', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([
{ source: 'website', count: 30 },
{ source: 'referral', count: 15 },
{ source: 'social', count: 5 },
]),
};
leadRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.getLeadsBySource(mockTenantId);
expect(result).toHaveLength(3);
expect(result[0].source).toBe('website');
expect(result[0].count).toBe(30);
expect(result[0].percentage).toBe(60); // 30/50 = 60%
});
});
describe('getOpportunitiesByStage', () => {
it('should return opportunities grouped by stage', async () => {
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([
{ stage: OpportunityStage.QUALIFICATION, count: 10, totalAmount: 100000 },
{ stage: OpportunityStage.PROPOSAL, count: 5, totalAmount: 75000 },
{ stage: OpportunityStage.CLOSED_WON, count: 3, totalAmount: 45000 },
]),
};
opportunityRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
const result = await service.getOpportunitiesByStage(mockTenantId);
expect(result).toHaveLength(3);
expect(result[0].stage).toBe(OpportunityStage.QUALIFICATION);
expect(result[0].count).toBe(10);
expect(result[0].totalAmount).toBe(100000);
expect(result[0].percentage).toBe(56); // 10/18 ≈ 56%
});
});
describe('getConversionFunnel', () => {
it('should return conversion funnel stages', async () => {
leadRepo.count
.mockResolvedValueOnce(100) // total leads
.mockResolvedValueOnce(40) // new leads
.mockResolvedValueOnce(30) // contacted
.mockResolvedValueOnce(20) // qualified
.mockResolvedValueOnce(10); // converted
opportunityRepo.count.mockResolvedValue(5); // won
const result = await service.getConversionFunnel(mockTenantId);
expect(result).toHaveLength(5);
expect(result[0].stage).toBe('New Leads');
expect(result[0].count).toBe(40);
expect(result[4].stage).toBe('Won Deals');
expect(result[4].count).toBe(5);
});
it('should handle zero leads in funnel', async () => {
leadRepo.count.mockResolvedValue(0);
opportunityRepo.count.mockResolvedValue(0);
const result = await service.getConversionFunnel(mockTenantId);
expect(result).toHaveLength(5);
result.forEach((stage) => {
expect(stage.percentage).toBe(0);
});
});
});
describe('getSalesPerformance', () => {
it('should return sales performance by user', async () => {
dataSource.query.mockResolvedValue([
{
user_id: 'user-001',
user_name: 'John Doe',
leads_assigned: 20,
opportunities_won: 5,
total_revenue: 50000,
activities_completed: 30,
},
{
user_id: 'user-002',
user_name: 'Jane Smith',
leads_assigned: 15,
opportunities_won: 3,
total_revenue: 35000,
activities_completed: 25,
},
]);
const result = await service.getSalesPerformance(mockTenantId);
expect(result).toHaveLength(2);
expect(result[0].userId).toBe('user-001');
expect(result[0].userName).toBe('John Doe');
expect(result[0].leadsAssigned).toBe(20);
expect(result[0].opportunitiesWon).toBe(5);
expect(result[0].totalRevenue).toBe(50000);
expect(result[0].conversionRate).toBe(25); // 5/20 = 25%
expect(result[0].activitiesCompleted).toBe(30);
});
it('should handle empty results', async () => {
dataSource.query.mockResolvedValue([]);
const result = await service.getSalesPerformance(mockTenantId);
expect(result).toHaveLength(0);
});
it('should handle user with zero leads', async () => {
dataSource.query.mockResolvedValue([
{
user_id: 'user-001',
user_name: 'New User',
leads_assigned: 0,
opportunities_won: 0,
total_revenue: 0,
activities_completed: 0,
},
]);
const result = await service.getSalesPerformance(mockTenantId);
expect(result[0].conversionRate).toBe(0);
});
});
});