feat(mlm,goals): Add unit tests for MLM and Goals modules
- Add 8 test files for MLM module (4 services, 4 controllers) - Add 4 test files for Goals module (2 services, 2 controllers) - Extract MLM enums to separate file to fix circular dependency - Total: 153 new tests Tested: - All MLM services: structures, ranks, nodes, commissions - All MLM controllers: structures, ranks, nodes, commissions - All Goals services: definitions, assignments - All Goals controllers: definitions, assignments Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
23bd170769
commit
a671697d35
336
src/modules/goals/__tests__/assignments.controller.spec.ts
Normal file
336
src/modules/goals/__tests__/assignments.controller.spec.ts
Normal file
@ -0,0 +1,336 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AssignmentsController } from '../controllers/assignments.controller';
|
||||
import { AssignmentsService } from '../services/assignments.service';
|
||||
import { AssigneeType, AssignmentStatus } from '../entities/assignment.entity';
|
||||
import { ProgressSource } from '../entities/progress-log.entity';
|
||||
import {
|
||||
CreateAssignmentDto,
|
||||
UpdateAssignmentDto,
|
||||
UpdateAssignmentStatusDto,
|
||||
UpdateProgressDto,
|
||||
AssignmentFiltersDto,
|
||||
} from '../dto/assignment.dto';
|
||||
|
||||
describe('AssignmentsController', () => {
|
||||
let controller: AssignmentsController;
|
||||
let service: jest.Mocked<AssignmentsService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockAssignmentId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
const mockDefinitionId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockAssignment = {
|
||||
id: mockAssignmentId,
|
||||
tenantId: mockTenantId,
|
||||
definitionId: mockDefinitionId,
|
||||
assigneeType: AssigneeType.USER,
|
||||
userId: mockUserId,
|
||||
teamId: null,
|
||||
customTarget: null,
|
||||
currentValue: 5000,
|
||||
progressPercentage: 50,
|
||||
lastUpdatedAt: new Date(),
|
||||
status: AssignmentStatus.ACTIVE,
|
||||
achievedAt: null,
|
||||
notes: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
definition: null as any,
|
||||
progressLogs: [],
|
||||
milestoneNotifications: [],
|
||||
};
|
||||
|
||||
const mockGoalsSummary = {
|
||||
totalAssignments: 5,
|
||||
activeAssignments: 3,
|
||||
achievedAssignments: 2,
|
||||
failedAssignments: 0,
|
||||
averageProgress: 65,
|
||||
atRiskCount: 1,
|
||||
};
|
||||
|
||||
const mockCompletionReport = {
|
||||
totalGoals: 10,
|
||||
achievedGoals: 6,
|
||||
failedGoals: 2,
|
||||
activeGoals: 2,
|
||||
completionRate: 75,
|
||||
averageProgress: 70,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findByDefinition: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
updateProgress: jest.fn(),
|
||||
getProgressHistory: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
getMyGoals: jest.fn(),
|
||||
getMyGoalsSummary: jest.fn(),
|
||||
getCompletionReport: jest.fn(),
|
||||
getUserReport: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AssignmentsController],
|
||||
providers: [{ provide: AssignmentsService, useValue: mockService }],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<AssignmentsController>(AssignmentsController);
|
||||
service = module.get(AssignmentsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('CRUD Operations', () => {
|
||||
describe('create', () => {
|
||||
it('should create a new assignment', async () => {
|
||||
const createDto: CreateAssignmentDto = {
|
||||
definitionId: mockDefinitionId,
|
||||
assigneeType: AssigneeType.USER,
|
||||
userId: mockUserId,
|
||||
};
|
||||
service.create.mockResolvedValue(mockAssignment);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(service.create).toHaveBeenCalledWith(mockTenantId, createDto);
|
||||
expect(result.userId).toBe(mockUserId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all assignments', async () => {
|
||||
const filters: AssignmentFiltersDto = {};
|
||||
service.findAll.mockResolvedValue({
|
||||
items: [mockAssignment],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
});
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
expect(result.items).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const filters: AssignmentFiltersDto = { status: AssignmentStatus.ACTIVE };
|
||||
service.findAll.mockResolvedValue({
|
||||
items: [mockAssignment],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
});
|
||||
|
||||
await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByDefinition', () => {
|
||||
it('should return assignments for a specific goal', async () => {
|
||||
service.findByDefinition.mockResolvedValue([mockAssignment]);
|
||||
|
||||
const result = await controller.findByDefinition(mockRequestUser, mockDefinitionId);
|
||||
|
||||
expect(service.findByDefinition).toHaveBeenCalledWith(mockTenantId, mockDefinitionId);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return an assignment by id', async () => {
|
||||
service.findOne.mockResolvedValue(mockAssignment);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockAssignmentId);
|
||||
|
||||
expect(service.findOne).toHaveBeenCalledWith(mockTenantId, mockAssignmentId);
|
||||
expect(result).toEqual(mockAssignment);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update an assignment', async () => {
|
||||
const updateDto: UpdateAssignmentDto = { customTarget: 15000 };
|
||||
service.update.mockResolvedValue({ ...mockAssignment, customTarget: 15000 });
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockAssignmentId, updateDto);
|
||||
|
||||
expect(service.update).toHaveBeenCalledWith(mockTenantId, mockAssignmentId, updateDto);
|
||||
expect(result.customTarget).toBe(15000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('should update assignment status', async () => {
|
||||
const statusDto: UpdateAssignmentStatusDto = { status: AssignmentStatus.ACHIEVED };
|
||||
service.updateStatus.mockResolvedValue({
|
||||
...mockAssignment,
|
||||
status: AssignmentStatus.ACHIEVED,
|
||||
achievedAt: new Date(),
|
||||
});
|
||||
|
||||
const result = await controller.updateStatus(mockRequestUser, mockAssignmentId, statusDto);
|
||||
|
||||
expect(service.updateStatus).toHaveBeenCalledWith(mockTenantId, mockAssignmentId, statusDto);
|
||||
expect(result.status).toBe(AssignmentStatus.ACHIEVED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateProgress', () => {
|
||||
it('should update assignment progress', async () => {
|
||||
const progressDto: UpdateProgressDto = { value: 7500, source: ProgressSource.MANUAL };
|
||||
service.updateProgress.mockResolvedValue({
|
||||
...mockAssignment,
|
||||
currentValue: 7500,
|
||||
progressPercentage: 75,
|
||||
});
|
||||
|
||||
const result = await controller.updateProgress(mockRequestUser, mockAssignmentId, progressDto);
|
||||
|
||||
expect(service.updateProgress).toHaveBeenCalledWith(
|
||||
mockTenantId,
|
||||
mockAssignmentId,
|
||||
mockUserId,
|
||||
progressDto,
|
||||
);
|
||||
expect(result.currentValue).toBe(7500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProgressHistory', () => {
|
||||
it('should return progress history', async () => {
|
||||
const mockHistory = [
|
||||
{ id: 'log-1', assignmentId: mockAssignmentId, previousValue: 0, newValue: 5000, changeAmount: 5000, source: ProgressSource.MANUAL, sourceReference: null, notes: null, loggedBy: mockUserId, loggedAt: new Date() },
|
||||
{ id: 'log-2', assignmentId: mockAssignmentId, previousValue: 5000, newValue: 7500, changeAmount: 2500, source: ProgressSource.MANUAL, sourceReference: null, notes: null, loggedBy: mockUserId, loggedAt: new Date() },
|
||||
];
|
||||
service.getProgressHistory.mockResolvedValue(mockHistory as any);
|
||||
|
||||
const result = await controller.getProgressHistory(mockRequestUser, mockAssignmentId);
|
||||
|
||||
expect(service.getProgressHistory).toHaveBeenCalledWith(mockTenantId, mockAssignmentId);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove an assignment', async () => {
|
||||
service.remove.mockResolvedValue(undefined);
|
||||
|
||||
await controller.remove(mockRequestUser, mockAssignmentId);
|
||||
|
||||
expect(service.remove).toHaveBeenCalledWith(mockTenantId, mockAssignmentId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('My Goals', () => {
|
||||
describe('getMyGoals', () => {
|
||||
it('should return current user goals', async () => {
|
||||
service.getMyGoals.mockResolvedValue([mockAssignment]);
|
||||
|
||||
const result = await controller.getMyGoals(mockRequestUser);
|
||||
|
||||
expect(service.getMyGoals).toHaveBeenCalledWith(mockTenantId, mockUserId);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMyGoalsSummary', () => {
|
||||
it('should return goals summary', async () => {
|
||||
service.getMyGoalsSummary.mockResolvedValue(mockGoalsSummary);
|
||||
|
||||
const result = await controller.getMyGoalsSummary(mockRequestUser);
|
||||
|
||||
expect(service.getMyGoalsSummary).toHaveBeenCalledWith(mockTenantId, mockUserId);
|
||||
expect(result.totalAssignments).toBe(5);
|
||||
expect(result.averageProgress).toBe(65);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMyProgress', () => {
|
||||
it('should update current user goal progress', async () => {
|
||||
const progressDto: UpdateProgressDto = { value: 6000 };
|
||||
service.updateProgress.mockResolvedValue({
|
||||
...mockAssignment,
|
||||
currentValue: 6000,
|
||||
progressPercentage: 60,
|
||||
});
|
||||
|
||||
const result = await controller.updateMyProgress(mockRequestUser, mockAssignmentId, progressDto);
|
||||
|
||||
expect(service.updateProgress).toHaveBeenCalledWith(
|
||||
mockTenantId,
|
||||
mockAssignmentId,
|
||||
mockUserId,
|
||||
progressDto,
|
||||
);
|
||||
expect(result.currentValue).toBe(6000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reports', () => {
|
||||
describe('getCompletionReport', () => {
|
||||
it('should return completion report without dates', async () => {
|
||||
service.getCompletionReport.mockResolvedValue(mockCompletionReport);
|
||||
|
||||
const result = await controller.getCompletionReport(mockRequestUser);
|
||||
|
||||
expect(service.getCompletionReport).toHaveBeenCalledWith(mockTenantId, undefined, undefined);
|
||||
expect(result.completionRate).toBe(75);
|
||||
});
|
||||
|
||||
it('should return completion report with date range', async () => {
|
||||
service.getCompletionReport.mockResolvedValue(mockCompletionReport);
|
||||
|
||||
const result = await controller.getCompletionReport(
|
||||
mockRequestUser,
|
||||
'2026-01-01',
|
||||
'2026-01-31',
|
||||
);
|
||||
|
||||
expect(service.getCompletionReport).toHaveBeenCalledWith(
|
||||
mockTenantId,
|
||||
new Date('2026-01-01'),
|
||||
new Date('2026-01-31'),
|
||||
);
|
||||
expect(result.totalGoals).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserReport', () => {
|
||||
it('should return report by user', async () => {
|
||||
const mockUserReport = [
|
||||
{ userId: mockUserId, userName: 'test@example.com', totalAssignments: 5, achieved: 3, failed: 1, active: 1, averageProgress: 70 },
|
||||
];
|
||||
service.getUserReport.mockResolvedValue(mockUserReport);
|
||||
|
||||
const result = await controller.getUserReport(mockRequestUser);
|
||||
|
||||
expect(service.getUserReport).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].totalAssignments).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
355
src/modules/goals/__tests__/assignments.service.spec.ts
Normal file
355
src/modules/goals/__tests__/assignments.service.spec.ts
Normal file
@ -0,0 +1,355 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { AssignmentsService } from '../services/assignments.service';
|
||||
import { AssignmentEntity, AssigneeType, AssignmentStatus } from '../entities/assignment.entity';
|
||||
import { ProgressLogEntity, ProgressSource } from '../entities/progress-log.entity';
|
||||
import { MilestoneNotificationEntity } from '../entities/milestone-notification.entity';
|
||||
import { DefinitionEntity, GoalStatus, GoalType, MetricType, PeriodType, DataSource } from '../entities/definition.entity';
|
||||
import { CreateAssignmentDto, UpdateProgressDto, AssignmentFiltersDto } from '../dto/assignment.dto';
|
||||
|
||||
describe('AssignmentsService', () => {
|
||||
let service: AssignmentsService;
|
||||
let assignmentRepo: jest.Mocked<Repository<AssignmentEntity>>;
|
||||
let progressLogRepo: jest.Mocked<Repository<ProgressLogEntity>>;
|
||||
let milestoneRepo: jest.Mocked<Repository<MilestoneNotificationEntity>>;
|
||||
let definitionRepo: jest.Mocked<Repository<DefinitionEntity>>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockAssignmentId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
const mockDefinitionId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
|
||||
const mockDefinition: DefinitionEntity = {
|
||||
id: mockDefinitionId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'Monthly Sales Target',
|
||||
description: null,
|
||||
category: 'Sales',
|
||||
type: GoalType.TARGET,
|
||||
metric: MetricType.CURRENCY,
|
||||
targetValue: 10000,
|
||||
unit: 'USD',
|
||||
period: PeriodType.MONTHLY,
|
||||
startsAt: new Date('2026-01-01'),
|
||||
endsAt: new Date('2026-01-31'),
|
||||
source: DataSource.SALES,
|
||||
sourceConfig: {},
|
||||
milestones: [{ percentage: 50, notify: true }, { percentage: 100, notify: true }],
|
||||
status: GoalStatus.ACTIVE,
|
||||
tags: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdBy: mockUserId,
|
||||
assignments: [],
|
||||
};
|
||||
|
||||
const mockAssignment: AssignmentEntity = {
|
||||
id: mockAssignmentId,
|
||||
tenantId: mockTenantId,
|
||||
definitionId: mockDefinitionId,
|
||||
assigneeType: AssigneeType.USER,
|
||||
userId: mockUserId,
|
||||
teamId: null,
|
||||
customTarget: null,
|
||||
currentValue: 5000,
|
||||
progressPercentage: 50,
|
||||
lastUpdatedAt: new Date(),
|
||||
status: AssignmentStatus.ACTIVE,
|
||||
achievedAt: null,
|
||||
notes: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
definition: mockDefinition,
|
||||
progressLogs: [],
|
||||
milestoneNotifications: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockQueryBuilder = {
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
innerJoin: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
skip: jest.fn().mockReturnThis(),
|
||||
take: jest.fn().mockReturnThis(),
|
||||
groupBy: jest.fn().mockReturnThis(),
|
||||
addGroupBy: jest.fn().mockReturnThis(),
|
||||
select: jest.fn().mockReturnThis(),
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
getManyAndCount: jest.fn().mockResolvedValue([[mockAssignment], 1]),
|
||||
getMany: jest.fn().mockResolvedValue([mockAssignment]),
|
||||
getRawMany: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
const mockAssignmentRepoObj = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => mockQueryBuilder),
|
||||
};
|
||||
|
||||
const mockProgressLogRepoObj = {
|
||||
find: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const mockMilestoneRepoObj = {
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const mockDefinitionRepoObj = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AssignmentsService,
|
||||
{ provide: getRepositoryToken(AssignmentEntity), useValue: mockAssignmentRepoObj },
|
||||
{ provide: getRepositoryToken(ProgressLogEntity), useValue: mockProgressLogRepoObj },
|
||||
{ provide: getRepositoryToken(MilestoneNotificationEntity), useValue: mockMilestoneRepoObj },
|
||||
{ provide: getRepositoryToken(DefinitionEntity), useValue: mockDefinitionRepoObj },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AssignmentsService>(AssignmentsService);
|
||||
assignmentRepo = module.get(getRepositoryToken(AssignmentEntity));
|
||||
progressLogRepo = module.get(getRepositoryToken(ProgressLogEntity));
|
||||
milestoneRepo = module.get(getRepositoryToken(MilestoneNotificationEntity));
|
||||
definitionRepo = module.get(getRepositoryToken(DefinitionEntity));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new assignment', async () => {
|
||||
const createDto: CreateAssignmentDto = {
|
||||
definitionId: mockDefinitionId,
|
||||
assigneeType: AssigneeType.USER,
|
||||
userId: mockUserId,
|
||||
};
|
||||
|
||||
definitionRepo.findOne.mockResolvedValue(mockDefinition);
|
||||
assignmentRepo.create.mockReturnValue(mockAssignment);
|
||||
assignmentRepo.save.mockResolvedValue(mockAssignment);
|
||||
|
||||
const result = await service.create(mockTenantId, createDto);
|
||||
|
||||
expect(definitionRepo.findOne).toHaveBeenCalledWith({
|
||||
where: { id: mockDefinitionId, tenantId: mockTenantId },
|
||||
});
|
||||
expect(result.userId).toBe(mockUserId);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if definition not found', async () => {
|
||||
definitionRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.create(mockTenantId, { definitionId: 'invalid', assigneeType: AssigneeType.USER }),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if user assignment without userId', async () => {
|
||||
definitionRepo.findOne.mockResolvedValue(mockDefinition);
|
||||
|
||||
await expect(
|
||||
service.create(mockTenantId, { definitionId: mockDefinitionId, assigneeType: AssigneeType.USER }),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if team assignment without teamId', async () => {
|
||||
definitionRepo.findOne.mockResolvedValue(mockDefinition);
|
||||
|
||||
await expect(
|
||||
service.create(mockTenantId, { definitionId: mockDefinitionId, assigneeType: AssigneeType.TEAM }),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated assignments', async () => {
|
||||
const filters: AssignmentFiltersDto = {};
|
||||
|
||||
const result = await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should filter by definitionId', async () => {
|
||||
const filters: AssignmentFiltersDto = { definitionId: mockDefinitionId };
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(assignmentRepo.createQueryBuilder).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const filters: AssignmentFiltersDto = { status: AssignmentStatus.ACTIVE };
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(assignmentRepo.createQueryBuilder).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return an assignment by id', async () => {
|
||||
assignmentRepo.findOne.mockResolvedValue(mockAssignment);
|
||||
|
||||
const result = await service.findOne(mockTenantId, mockAssignmentId);
|
||||
|
||||
expect(assignmentRepo.findOne).toHaveBeenCalledWith({
|
||||
where: { id: mockAssignmentId, tenantId: mockTenantId },
|
||||
relations: ['definition', 'user'],
|
||||
});
|
||||
expect(result).toEqual(mockAssignment);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if not found', async () => {
|
||||
assignmentRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne(mockTenantId, 'non-existent')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateProgress', () => {
|
||||
it('should update progress and log the change', async () => {
|
||||
const progressDto: UpdateProgressDto = { value: 7500 };
|
||||
assignmentRepo.findOne.mockResolvedValue(mockAssignment);
|
||||
progressLogRepo.create.mockReturnValue({} as ProgressLogEntity);
|
||||
progressLogRepo.save.mockResolvedValue({} as ProgressLogEntity);
|
||||
milestoneRepo.findOne.mockResolvedValue(null);
|
||||
assignmentRepo.save.mockResolvedValue({
|
||||
...mockAssignment,
|
||||
currentValue: 7500,
|
||||
progressPercentage: 75,
|
||||
});
|
||||
|
||||
const result = await service.updateProgress(mockTenantId, mockAssignmentId, mockUserId, progressDto);
|
||||
|
||||
expect(progressLogRepo.create).toHaveBeenCalled();
|
||||
expect(progressLogRepo.save).toHaveBeenCalled();
|
||||
expect(result.currentValue).toBe(7500);
|
||||
});
|
||||
|
||||
it('should auto-achieve when progress reaches 100%', async () => {
|
||||
const progressDto: UpdateProgressDto = { value: 10000 };
|
||||
assignmentRepo.findOne.mockResolvedValue(mockAssignment);
|
||||
progressLogRepo.create.mockReturnValue({} as ProgressLogEntity);
|
||||
progressLogRepo.save.mockResolvedValue({} as ProgressLogEntity);
|
||||
milestoneRepo.findOne.mockResolvedValue(null);
|
||||
assignmentRepo.save.mockResolvedValue({
|
||||
...mockAssignment,
|
||||
currentValue: 10000,
|
||||
progressPercentage: 100,
|
||||
status: AssignmentStatus.ACHIEVED,
|
||||
achievedAt: new Date(),
|
||||
});
|
||||
|
||||
const result = await service.updateProgress(mockTenantId, mockAssignmentId, mockUserId, progressDto);
|
||||
|
||||
expect(result.status).toBe(AssignmentStatus.ACHIEVED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProgressHistory', () => {
|
||||
it('should return progress history', async () => {
|
||||
const mockLogs = [
|
||||
{ id: 'log-1', previousValue: 0, newValue: 5000 },
|
||||
{ id: 'log-2', previousValue: 5000, newValue: 7500 },
|
||||
];
|
||||
progressLogRepo.find.mockResolvedValue(mockLogs as ProgressLogEntity[]);
|
||||
|
||||
const result = await service.getProgressHistory(mockTenantId, mockAssignmentId);
|
||||
|
||||
expect(progressLogRepo.find).toHaveBeenCalledWith({
|
||||
where: { tenantId: mockTenantId, assignmentId: mockAssignmentId },
|
||||
order: { loggedAt: 'DESC' },
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMyGoals', () => {
|
||||
it('should return user goals', async () => {
|
||||
assignmentRepo.find.mockResolvedValue([mockAssignment]);
|
||||
|
||||
const result = await service.getMyGoals(mockTenantId, mockUserId);
|
||||
|
||||
expect(assignmentRepo.find).toHaveBeenCalledWith({
|
||||
where: { tenantId: mockTenantId, userId: mockUserId },
|
||||
relations: ['definition'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMyGoalsSummary', () => {
|
||||
it('should return goals summary', async () => {
|
||||
const mockAssignments = [
|
||||
{ ...mockAssignment, status: AssignmentStatus.ACTIVE, progressPercentage: 50 },
|
||||
{ ...mockAssignment, id: 'a2', status: AssignmentStatus.ACHIEVED, progressPercentage: 100 },
|
||||
];
|
||||
assignmentRepo.find.mockResolvedValue(mockAssignments as AssignmentEntity[]);
|
||||
|
||||
const result = await service.getMyGoalsSummary(mockTenantId, mockUserId);
|
||||
|
||||
expect(result.totalAssignments).toBe(2);
|
||||
expect(result.activeAssignments).toBe(1);
|
||||
expect(result.achievedAssignments).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCompletionReport', () => {
|
||||
it('should return completion report', async () => {
|
||||
const mockQueryBuilder = assignmentRepo.createQueryBuilder();
|
||||
(mockQueryBuilder.getMany as jest.Mock).mockResolvedValue([
|
||||
{ ...mockAssignment, status: AssignmentStatus.ACHIEVED, progressPercentage: 100 },
|
||||
{ ...mockAssignment, id: 'a2', status: AssignmentStatus.FAILED, progressPercentage: 40 },
|
||||
]);
|
||||
|
||||
const result = await service.getCompletionReport(mockTenantId);
|
||||
|
||||
expect(result.totalGoals).toBe(2);
|
||||
expect(result.achievedGoals).toBe(1);
|
||||
expect(result.failedGoals).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserReport', () => {
|
||||
it('should return report grouped by user', async () => {
|
||||
const mockResult = [
|
||||
{ userId: mockUserId, userName: 'test@example.com', totalAssignments: '2', achieved: '1', failed: '0', active: '1', averageProgress: '75.00' },
|
||||
];
|
||||
const mockQueryBuilder = assignmentRepo.createQueryBuilder();
|
||||
(mockQueryBuilder.getRawMany as jest.Mock).mockResolvedValue(mockResult);
|
||||
|
||||
const result = await service.getUserReport(mockTenantId);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].totalAssignments).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove an assignment', async () => {
|
||||
assignmentRepo.findOne.mockResolvedValue(mockAssignment);
|
||||
assignmentRepo.remove.mockResolvedValue(mockAssignment);
|
||||
|
||||
await service.remove(mockTenantId, mockAssignmentId);
|
||||
|
||||
expect(assignmentRepo.remove).toHaveBeenCalledWith(mockAssignment);
|
||||
});
|
||||
});
|
||||
});
|
||||
218
src/modules/goals/__tests__/definitions.controller.spec.ts
Normal file
218
src/modules/goals/__tests__/definitions.controller.spec.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DefinitionsController } from '../controllers/definitions.controller';
|
||||
import { DefinitionsService } from '../services/definitions.service';
|
||||
import {
|
||||
GoalType,
|
||||
MetricType,
|
||||
PeriodType,
|
||||
GoalStatus,
|
||||
DataSource,
|
||||
} from '../entities/definition.entity';
|
||||
import {
|
||||
CreateDefinitionDto,
|
||||
UpdateDefinitionDto,
|
||||
UpdateDefinitionStatusDto,
|
||||
DefinitionFiltersDto,
|
||||
} from '../dto/definition.dto';
|
||||
|
||||
describe('DefinitionsController', () => {
|
||||
let controller: DefinitionsController;
|
||||
let service: jest.Mocked<DefinitionsService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockDefinitionId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockDefinition = {
|
||||
id: mockDefinitionId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'Monthly Sales Target',
|
||||
description: 'Achieve $50,000 in sales',
|
||||
category: 'Sales',
|
||||
type: GoalType.TARGET,
|
||||
metric: MetricType.CURRENCY,
|
||||
targetValue: 50000,
|
||||
unit: 'USD',
|
||||
period: PeriodType.MONTHLY,
|
||||
startsAt: new Date('2026-01-01'),
|
||||
endsAt: new Date('2026-01-31'),
|
||||
source: DataSource.SALES,
|
||||
sourceConfig: {},
|
||||
milestones: [{ percentage: 50, notify: true }],
|
||||
tags: ['sales'],
|
||||
status: GoalStatus.ACTIVE,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdBy: mockUserId,
|
||||
assignments: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
activate: jest.fn(),
|
||||
duplicate: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [DefinitionsController],
|
||||
providers: [{ provide: DefinitionsService, useValue: mockService }],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<DefinitionsController>(DefinitionsController);
|
||||
service = module.get(DefinitionsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new definition', async () => {
|
||||
const createDto: CreateDefinitionDto = {
|
||||
name: 'New Goal',
|
||||
targetValue: 10000,
|
||||
startsAt: new Date('2026-02-01'),
|
||||
endsAt: new Date('2026-02-28'),
|
||||
};
|
||||
service.create.mockResolvedValue({ ...mockDefinition, ...createDto });
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(service.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result.name).toBe('New Goal');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all definitions', async () => {
|
||||
const filters: DefinitionFiltersDto = {};
|
||||
service.findAll.mockResolvedValue({
|
||||
items: [mockDefinition],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
});
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
expect(result.items).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const filters: DefinitionFiltersDto = { status: GoalStatus.ACTIVE };
|
||||
service.findAll.mockResolvedValue({
|
||||
items: [mockDefinition],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
});
|
||||
|
||||
await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
});
|
||||
|
||||
it('should filter by period', async () => {
|
||||
const filters: DefinitionFiltersDto = { period: PeriodType.MONTHLY };
|
||||
service.findAll.mockResolvedValue({
|
||||
items: [mockDefinition],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
});
|
||||
|
||||
await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a definition by id', async () => {
|
||||
service.findOne.mockResolvedValue(mockDefinition);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockDefinitionId);
|
||||
|
||||
expect(service.findOne).toHaveBeenCalledWith(mockTenantId, mockDefinitionId);
|
||||
expect(result).toEqual(mockDefinition);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a definition', async () => {
|
||||
const updateDto: UpdateDefinitionDto = { name: 'Updated Goal' };
|
||||
service.update.mockResolvedValue({ ...mockDefinition, name: 'Updated Goal' });
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockDefinitionId, updateDto);
|
||||
|
||||
expect(service.update).toHaveBeenCalledWith(mockTenantId, mockDefinitionId, updateDto);
|
||||
expect(result.name).toBe('Updated Goal');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('should update definition status', async () => {
|
||||
const statusDto: UpdateDefinitionStatusDto = { status: GoalStatus.PAUSED };
|
||||
service.updateStatus.mockResolvedValue({ ...mockDefinition, status: GoalStatus.PAUSED });
|
||||
|
||||
const result = await controller.updateStatus(mockRequestUser, mockDefinitionId, statusDto);
|
||||
|
||||
expect(service.updateStatus).toHaveBeenCalledWith(mockTenantId, mockDefinitionId, statusDto);
|
||||
expect(result.status).toBe(GoalStatus.PAUSED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activate', () => {
|
||||
it('should activate a definition', async () => {
|
||||
service.activate.mockResolvedValue({ ...mockDefinition, status: GoalStatus.ACTIVE });
|
||||
|
||||
const result = await controller.activate(mockRequestUser, mockDefinitionId);
|
||||
|
||||
expect(service.activate).toHaveBeenCalledWith(mockTenantId, mockDefinitionId);
|
||||
expect(result.status).toBe(GoalStatus.ACTIVE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicate', () => {
|
||||
it('should duplicate a definition', async () => {
|
||||
service.duplicate.mockResolvedValue({
|
||||
...mockDefinition,
|
||||
id: 'new-id',
|
||||
name: 'Monthly Sales Target (Copy)',
|
||||
status: GoalStatus.DRAFT,
|
||||
});
|
||||
|
||||
const result = await controller.duplicate(mockRequestUser, mockDefinitionId);
|
||||
|
||||
expect(service.duplicate).toHaveBeenCalledWith(mockTenantId, mockDefinitionId, mockUserId);
|
||||
expect(result.name).toContain('(Copy)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove a definition', async () => {
|
||||
service.remove.mockResolvedValue(undefined);
|
||||
|
||||
await controller.remove(mockRequestUser, mockDefinitionId);
|
||||
|
||||
expect(service.remove).toHaveBeenCalledWith(mockTenantId, mockDefinitionId);
|
||||
});
|
||||
});
|
||||
});
|
||||
283
src/modules/goals/__tests__/definitions.service.spec.ts
Normal file
283
src/modules/goals/__tests__/definitions.service.spec.ts
Normal file
@ -0,0 +1,283 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { DefinitionsService } from '../services/definitions.service';
|
||||
import {
|
||||
DefinitionEntity,
|
||||
GoalType,
|
||||
MetricType,
|
||||
PeriodType,
|
||||
DataSource,
|
||||
GoalStatus,
|
||||
} from '../entities/definition.entity';
|
||||
import { CreateDefinitionDto, UpdateDefinitionDto, DefinitionFiltersDto } from '../dto/definition.dto';
|
||||
|
||||
describe('DefinitionsService', () => {
|
||||
let service: DefinitionsService;
|
||||
let repository: jest.Mocked<Repository<DefinitionEntity>>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockDefinitionId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockDefinition: DefinitionEntity = {
|
||||
id: mockDefinitionId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'Monthly Sales Target',
|
||||
description: 'Achieve $50,000 in sales',
|
||||
category: 'Sales',
|
||||
type: GoalType.TARGET,
|
||||
metric: MetricType.CURRENCY,
|
||||
targetValue: 50000,
|
||||
unit: 'USD',
|
||||
period: PeriodType.MONTHLY,
|
||||
startsAt: new Date('2026-01-01'),
|
||||
endsAt: new Date('2026-01-31'),
|
||||
source: DataSource.SALES,
|
||||
sourceConfig: { module: 'sales', entity: 'opportunity', aggregation: 'sum', field: 'value' },
|
||||
milestones: [{ percentage: 50, notify: true }, { percentage: 100, notify: true }],
|
||||
status: GoalStatus.ACTIVE,
|
||||
tags: ['sales', 'monthly'],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdBy: mockUserId,
|
||||
assignments: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockQueryBuilder = {
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
loadRelationCountAndMap: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
skip: jest.fn().mockReturnThis(),
|
||||
take: jest.fn().mockReturnThis(),
|
||||
getManyAndCount: jest.fn().mockResolvedValue([[mockDefinition], 1]),
|
||||
};
|
||||
|
||||
const mockRepo = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => mockQueryBuilder),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DefinitionsService,
|
||||
{ provide: getRepositoryToken(DefinitionEntity), useValue: mockRepo },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DefinitionsService>(DefinitionsService);
|
||||
repository = module.get(getRepositoryToken(DefinitionEntity));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new definition', async () => {
|
||||
const createDto: CreateDefinitionDto = {
|
||||
name: 'New Goal',
|
||||
targetValue: 10000,
|
||||
startsAt: new Date('2026-02-01'),
|
||||
endsAt: new Date('2026-02-28'),
|
||||
};
|
||||
|
||||
repository.create.mockReturnValue({ ...mockDefinition, ...createDto });
|
||||
repository.save.mockResolvedValue({ ...mockDefinition, ...createDto });
|
||||
|
||||
const result = await service.create(mockTenantId, mockUserId, createDto);
|
||||
|
||||
expect(repository.create).toHaveBeenCalledWith({
|
||||
tenantId: mockTenantId,
|
||||
createdBy: mockUserId,
|
||||
...createDto,
|
||||
});
|
||||
expect(result.name).toBe('New Goal');
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if end date before start date', async () => {
|
||||
const createDto: CreateDefinitionDto = {
|
||||
name: 'Invalid Goal',
|
||||
targetValue: 10000,
|
||||
startsAt: new Date('2026-02-28'),
|
||||
endsAt: new Date('2026-02-01'),
|
||||
};
|
||||
|
||||
await expect(service.create(mockTenantId, mockUserId, createDto)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated definitions', async () => {
|
||||
const filters: DefinitionFiltersDto = {};
|
||||
|
||||
const result = await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.total).toBe(1);
|
||||
expect(result.page).toBe(1);
|
||||
expect(result.limit).toBe(20);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const filters: DefinitionFiltersDto = { status: GoalStatus.ACTIVE };
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
const queryBuilder = repository.createQueryBuilder();
|
||||
expect(queryBuilder.andWhere).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should filter by period', async () => {
|
||||
const filters: DefinitionFiltersDto = { period: PeriodType.MONTHLY };
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(repository.createQueryBuilder).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should support search by name', async () => {
|
||||
const filters: DefinitionFiltersDto = { search: 'Sales' };
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(repository.createQueryBuilder).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a definition by id', async () => {
|
||||
repository.findOne.mockResolvedValue(mockDefinition);
|
||||
|
||||
const result = await service.findOne(mockTenantId, mockDefinitionId);
|
||||
|
||||
expect(repository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: mockDefinitionId, tenantId: mockTenantId },
|
||||
relations: ['creator'],
|
||||
});
|
||||
expect(result).toEqual(mockDefinition);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if not found', async () => {
|
||||
repository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne(mockTenantId, 'non-existent')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a definition', async () => {
|
||||
const updateDto: UpdateDefinitionDto = { name: 'Updated Goal' };
|
||||
repository.findOne.mockResolvedValue(mockDefinition);
|
||||
repository.save.mockResolvedValue({ ...mockDefinition, name: 'Updated Goal' });
|
||||
|
||||
const result = await service.update(mockTenantId, mockDefinitionId, updateDto);
|
||||
|
||||
expect(result.name).toBe('Updated Goal');
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if start date after existing end date', async () => {
|
||||
const updateDto: UpdateDefinitionDto = { startsAt: new Date('2026-12-01') };
|
||||
repository.findOne.mockResolvedValue(mockDefinition);
|
||||
|
||||
await expect(service.update(mockTenantId, mockDefinitionId, updateDto)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if end date before existing start date', async () => {
|
||||
const updateDto: UpdateDefinitionDto = { endsAt: new Date('2025-01-01') };
|
||||
repository.findOne.mockResolvedValue(mockDefinition);
|
||||
|
||||
await expect(service.update(mockTenantId, mockDefinitionId, updateDto)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('should update definition status', async () => {
|
||||
repository.findOne.mockResolvedValue(mockDefinition);
|
||||
repository.save.mockResolvedValue({ ...mockDefinition, status: GoalStatus.PAUSED });
|
||||
|
||||
const result = await service.updateStatus(mockTenantId, mockDefinitionId, {
|
||||
status: GoalStatus.PAUSED,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(GoalStatus.PAUSED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activate', () => {
|
||||
it('should activate a definition', async () => {
|
||||
const draftDefinition = { ...mockDefinition, status: GoalStatus.DRAFT };
|
||||
repository.findOne.mockResolvedValue(draftDefinition);
|
||||
repository.save.mockResolvedValue({ ...draftDefinition, status: GoalStatus.ACTIVE });
|
||||
|
||||
const result = await service.activate(mockTenantId, mockDefinitionId);
|
||||
|
||||
expect(result.status).toBe(GoalStatus.ACTIVE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicate', () => {
|
||||
it('should duplicate a definition', async () => {
|
||||
repository.findOne.mockResolvedValue(mockDefinition);
|
||||
repository.create.mockReturnValue({
|
||||
...mockDefinition,
|
||||
id: 'new-id',
|
||||
name: 'Monthly Sales Target (Copy)',
|
||||
status: GoalStatus.DRAFT,
|
||||
});
|
||||
repository.save.mockResolvedValue({
|
||||
...mockDefinition,
|
||||
id: 'new-id',
|
||||
name: 'Monthly Sales Target (Copy)',
|
||||
status: GoalStatus.DRAFT,
|
||||
});
|
||||
|
||||
const result = await service.duplicate(mockTenantId, mockDefinitionId, mockUserId);
|
||||
|
||||
expect(result.name).toContain('(Copy)');
|
||||
expect(result.status).toBe(GoalStatus.DRAFT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove a definition', async () => {
|
||||
repository.findOne.mockResolvedValue(mockDefinition);
|
||||
repository.remove.mockResolvedValue(mockDefinition);
|
||||
|
||||
await service.remove(mockTenantId, mockDefinitionId);
|
||||
|
||||
expect(repository.remove).toHaveBeenCalledWith(mockDefinition);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveGoals', () => {
|
||||
it('should return active goals within date range', async () => {
|
||||
repository.find.mockResolvedValue([mockDefinition]);
|
||||
|
||||
const result = await service.getActiveGoals(mockTenantId);
|
||||
|
||||
expect(repository.find).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
tenantId: mockTenantId,
|
||||
status: GoalStatus.ACTIVE,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
205
src/modules/mlm/__tests__/commissions.controller.spec.ts
Normal file
205
src/modules/mlm/__tests__/commissions.controller.spec.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CommissionsController } from '../controllers/commissions.controller';
|
||||
import { CommissionsService } from '../services/commissions.service';
|
||||
import {
|
||||
CalculateCommissionsDto,
|
||||
UpdateCommissionStatusDto,
|
||||
CommissionFiltersDto,
|
||||
} from '../dto/commission.dto';
|
||||
import { CommissionType, CommissionStatus } from '../entities/commission.entity';
|
||||
|
||||
describe('CommissionsController', () => {
|
||||
let controller: CommissionsController;
|
||||
let service: jest.Mocked<CommissionsService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockCommissionId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
const mockNodeId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockCommission = {
|
||||
id: mockCommissionId,
|
||||
tenantId: mockTenantId,
|
||||
nodeId: mockNodeId,
|
||||
sourceNodeId: 'source-node-id',
|
||||
type: CommissionType.LEVEL,
|
||||
level: 1,
|
||||
sourceAmount: 1000,
|
||||
rateApplied: 0.1,
|
||||
commissionAmount: 100,
|
||||
currency: 'USD',
|
||||
periodId: null,
|
||||
sourceReference: 'order-123',
|
||||
status: CommissionStatus.PENDING,
|
||||
paidAt: null,
|
||||
notes: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
node: null as any,
|
||||
sourceNode: null as any,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
calculateCommissions: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
getCommissionsByLevel: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [CommissionsController],
|
||||
providers: [{ provide: CommissionsService, useValue: mockService }],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<CommissionsController>(CommissionsController);
|
||||
service = module.get(CommissionsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all commissions', async () => {
|
||||
const filters: CommissionFiltersDto = {};
|
||||
service.findAll.mockResolvedValue({ items: [mockCommission], total: 1 });
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
expect(result.items).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should filter by nodeId', async () => {
|
||||
const filters: CommissionFiltersDto = { nodeId: mockNodeId };
|
||||
service.findAll.mockResolvedValue({ items: [mockCommission], total: 1 });
|
||||
|
||||
await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
});
|
||||
|
||||
it('should filter by type', async () => {
|
||||
const filters: CommissionFiltersDto = { type: CommissionType.LEVEL };
|
||||
service.findAll.mockResolvedValue({ items: [mockCommission], total: 1 });
|
||||
|
||||
await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const filters: CommissionFiltersDto = { status: CommissionStatus.PENDING };
|
||||
service.findAll.mockResolvedValue({ items: [mockCommission], total: 1 });
|
||||
|
||||
await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getByLevel', () => {
|
||||
it('should return commissions grouped by level', async () => {
|
||||
const mockResult = [
|
||||
{ level: 1, count: 5, totalAmount: 500 },
|
||||
{ level: 2, count: 3, totalAmount: 150 },
|
||||
];
|
||||
service.getCommissionsByLevel.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.getByLevel(mockRequestUser);
|
||||
|
||||
expect(service.getCommissionsByLevel).toHaveBeenCalledWith(mockTenantId, undefined);
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
|
||||
it('should filter by nodeId', async () => {
|
||||
const mockResult = [{ level: 1, count: 2, totalAmount: 200 }];
|
||||
service.getCommissionsByLevel.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.getByLevel(mockRequestUser, mockNodeId);
|
||||
|
||||
expect(service.getCommissionsByLevel).toHaveBeenCalledWith(mockTenantId, mockNodeId);
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a commission by id', async () => {
|
||||
service.findOne.mockResolvedValue(mockCommission);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockCommissionId);
|
||||
|
||||
expect(service.findOne).toHaveBeenCalledWith(mockTenantId, mockCommissionId);
|
||||
expect(result).toEqual(mockCommission);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateCommissions', () => {
|
||||
it('should calculate commissions for a sale', async () => {
|
||||
const dto: CalculateCommissionsDto = {
|
||||
sourceNodeId: 'source-node',
|
||||
amount: 1000,
|
||||
currency: 'USD',
|
||||
};
|
||||
service.calculateCommissions.mockResolvedValue([mockCommission]);
|
||||
|
||||
const result = await controller.calculateCommissions(mockRequestUser, dto);
|
||||
|
||||
expect(service.calculateCommissions).toHaveBeenCalledWith(mockTenantId, dto);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should calculate commissions with source reference', async () => {
|
||||
const dto: CalculateCommissionsDto = {
|
||||
sourceNodeId: 'source-node',
|
||||
amount: 500,
|
||||
sourceReference: 'order-456',
|
||||
};
|
||||
service.calculateCommissions.mockResolvedValue([
|
||||
{ ...mockCommission, sourceAmount: 500, commissionAmount: 50 },
|
||||
]);
|
||||
|
||||
const result = await controller.calculateCommissions(mockRequestUser, dto);
|
||||
|
||||
expect(service.calculateCommissions).toHaveBeenCalledWith(mockTenantId, dto);
|
||||
expect(result[0].sourceAmount).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('should update commission status to PAID', async () => {
|
||||
const dto: UpdateCommissionStatusDto = { status: CommissionStatus.PAID };
|
||||
service.updateStatus.mockResolvedValue({
|
||||
...mockCommission,
|
||||
status: CommissionStatus.PAID,
|
||||
paidAt: new Date(),
|
||||
});
|
||||
|
||||
const result = await controller.updateStatus(mockRequestUser, mockCommissionId, dto);
|
||||
|
||||
expect(service.updateStatus).toHaveBeenCalledWith(mockTenantId, mockCommissionId, dto);
|
||||
expect(result.status).toBe(CommissionStatus.PAID);
|
||||
});
|
||||
|
||||
it('should update commission status to CANCELLED', async () => {
|
||||
const dto: UpdateCommissionStatusDto = { status: CommissionStatus.CANCELLED };
|
||||
service.updateStatus.mockResolvedValue({
|
||||
...mockCommission,
|
||||
status: CommissionStatus.CANCELLED,
|
||||
});
|
||||
|
||||
const result = await controller.updateStatus(mockRequestUser, mockCommissionId, dto);
|
||||
|
||||
expect(result.status).toBe(CommissionStatus.CANCELLED);
|
||||
});
|
||||
});
|
||||
});
|
||||
337
src/modules/mlm/__tests__/commissions.service.spec.ts
Normal file
337
src/modules/mlm/__tests__/commissions.service.spec.ts
Normal file
@ -0,0 +1,337 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { CommissionsService } from '../services/commissions.service';
|
||||
import { CommissionEntity, CommissionType, CommissionStatus } from '../entities/commission.entity';
|
||||
import { BonusEntity, BonusType } from '../entities/bonus.entity';
|
||||
import { NodeEntity, NodeStatus } from '../entities/node.entity';
|
||||
import { StructureEntity, StructureType } from '../entities/structure.entity';
|
||||
import { CommissionFiltersDto, CalculateCommissionsDto } from '../dto/commission.dto';
|
||||
|
||||
describe('CommissionsService', () => {
|
||||
let service: CommissionsService;
|
||||
let commissionRepository: jest.Mocked<Repository<CommissionEntity>>;
|
||||
let bonusRepository: jest.Mocked<Repository<BonusEntity>>;
|
||||
let nodeRepository: jest.Mocked<Repository<NodeEntity>>;
|
||||
let structureRepository: jest.Mocked<Repository<StructureEntity>>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockNodeId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockCommissionId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
const mockStructureId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
|
||||
const mockCommission = {
|
||||
id: mockCommissionId,
|
||||
tenantId: mockTenantId,
|
||||
nodeId: mockNodeId,
|
||||
sourceNodeId: 'source-node-id',
|
||||
type: CommissionType.LEVEL,
|
||||
level: 1,
|
||||
sourceAmount: 1000,
|
||||
rateApplied: 0.1,
|
||||
commissionAmount: 100,
|
||||
currency: 'USD',
|
||||
periodId: null,
|
||||
sourceReference: 'order-123',
|
||||
status: CommissionStatus.PENDING,
|
||||
paidAt: null,
|
||||
notes: null,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
node: null as any,
|
||||
sourceNode: null as any,
|
||||
} as CommissionEntity;
|
||||
|
||||
const mockNode = {
|
||||
id: mockNodeId,
|
||||
tenantId: mockTenantId,
|
||||
structureId: mockStructureId,
|
||||
userId: 'user-001',
|
||||
parentId: null,
|
||||
sponsorId: null,
|
||||
position: 1,
|
||||
path: 'root.parent.node',
|
||||
depth: 2,
|
||||
rankId: null,
|
||||
highestRankId: null,
|
||||
personalVolume: 1000,
|
||||
groupVolume: 5000,
|
||||
directReferrals: 5,
|
||||
totalDownline: 10,
|
||||
totalEarnings: 500,
|
||||
status: NodeStatus.ACTIVE,
|
||||
inviteCode: 'ABC123',
|
||||
joinedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
structure: null as any,
|
||||
parent: null,
|
||||
sponsor: null,
|
||||
children: [],
|
||||
referrals: [],
|
||||
rank: null,
|
||||
highestRank: null,
|
||||
commissionsReceived: [],
|
||||
commissionsGenerated: [],
|
||||
bonuses: [],
|
||||
rankHistory: [],
|
||||
} as NodeEntity;
|
||||
|
||||
const mockStructure = {
|
||||
id: mockStructureId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'Test Structure',
|
||||
description: null,
|
||||
type: StructureType.UNILEVEL,
|
||||
levelRates: [
|
||||
{ level: 1, rate: 0.1 },
|
||||
{ level: 2, rate: 0.05 },
|
||||
],
|
||||
matchingRates: [],
|
||||
config: { maxDepth: 10 },
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdBy: null,
|
||||
nodes: [],
|
||||
ranks: [],
|
||||
} as StructureEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockCommissionRepo = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findAndCount: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
select: jest.fn().mockReturnThis(),
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
groupBy: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
getRawMany: jest.fn().mockResolvedValue([]),
|
||||
getRawOne: jest.fn().mockResolvedValue({ total: '100', pending: '50', paid: '50' }),
|
||||
})),
|
||||
};
|
||||
|
||||
const mockBonusRepo = {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
select: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
getRawOne: jest.fn().mockResolvedValue({ total: '50' }),
|
||||
})),
|
||||
};
|
||||
|
||||
const mockNodeRepo = {
|
||||
findOne: jest.fn(),
|
||||
increment: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn().mockResolvedValue([]),
|
||||
})),
|
||||
};
|
||||
|
||||
const mockStructureRepo = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
CommissionsService,
|
||||
{ provide: getRepositoryToken(CommissionEntity), useValue: mockCommissionRepo },
|
||||
{ provide: getRepositoryToken(BonusEntity), useValue: mockBonusRepo },
|
||||
{ provide: getRepositoryToken(NodeEntity), useValue: mockNodeRepo },
|
||||
{ provide: getRepositoryToken(StructureEntity), useValue: mockStructureRepo },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CommissionsService>(CommissionsService);
|
||||
commissionRepository = module.get(getRepositoryToken(CommissionEntity));
|
||||
bonusRepository = module.get(getRepositoryToken(BonusEntity));
|
||||
nodeRepository = module.get(getRepositoryToken(NodeEntity));
|
||||
structureRepository = module.get(getRepositoryToken(StructureEntity));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated commissions', async () => {
|
||||
commissionRepository.findAndCount.mockResolvedValue([[mockCommission], 1]);
|
||||
|
||||
const result = await service.findAll(mockTenantId);
|
||||
|
||||
expect(commissionRepository.findAndCount).toHaveBeenCalled();
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should filter by nodeId', async () => {
|
||||
const filters: CommissionFiltersDto = { nodeId: mockNodeId };
|
||||
commissionRepository.findAndCount.mockResolvedValue([[mockCommission], 1]);
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(commissionRepository.findAndCount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ nodeId: mockNodeId }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by type', async () => {
|
||||
const filters: CommissionFiltersDto = { type: CommissionType.LEVEL };
|
||||
commissionRepository.findAndCount.mockResolvedValue([[mockCommission], 1]);
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(commissionRepository.findAndCount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ type: CommissionType.LEVEL }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const filters: CommissionFiltersDto = { status: CommissionStatus.PENDING };
|
||||
commissionRepository.findAndCount.mockResolvedValue([[mockCommission], 1]);
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(commissionRepository.findAndCount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ status: CommissionStatus.PENDING }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a commission by id', async () => {
|
||||
commissionRepository.findOne.mockResolvedValue(mockCommission);
|
||||
|
||||
const result = await service.findOne(mockTenantId, mockCommissionId);
|
||||
|
||||
expect(commissionRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: mockCommissionId, tenantId: mockTenantId },
|
||||
});
|
||||
expect(result).toEqual(mockCommission);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if commission not found', async () => {
|
||||
commissionRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne(mockTenantId, 'non-existent')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateCommissions', () => {
|
||||
it('should calculate level commissions', async () => {
|
||||
const dto: CalculateCommissionsDto = {
|
||||
sourceNodeId: 'source-node',
|
||||
amount: 1000,
|
||||
currency: 'USD',
|
||||
};
|
||||
|
||||
nodeRepository.findOne.mockResolvedValue(mockNode);
|
||||
structureRepository.findOne.mockResolvedValue(mockStructure);
|
||||
commissionRepository.create.mockReturnValue(mockCommission);
|
||||
commissionRepository.save.mockResolvedValue([mockCommission] as any);
|
||||
|
||||
const result = await service.calculateCommissions(mockTenantId, dto);
|
||||
|
||||
expect(nodeRepository.findOne).toHaveBeenCalled();
|
||||
expect(structureRepository.findOne).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if source node not found', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.calculateCommissions(mockTenantId, { sourceNodeId: 'invalid', amount: 100 }),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('should update commission status to PAID', async () => {
|
||||
commissionRepository.findOne.mockResolvedValue(mockCommission);
|
||||
commissionRepository.save.mockResolvedValue({
|
||||
...mockCommission,
|
||||
status: CommissionStatus.PAID,
|
||||
paidAt: expect.any(Date),
|
||||
});
|
||||
|
||||
const result = await service.updateStatus(mockTenantId, mockCommissionId, {
|
||||
status: CommissionStatus.PAID,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(CommissionStatus.PAID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCommissionsByLevel', () => {
|
||||
it('should return commissions grouped by level', async () => {
|
||||
const mockResult = [{ level: '1', count: '5', totalAmount: '500' }];
|
||||
(commissionRepository.createQueryBuilder as jest.Mock).mockReturnValue({
|
||||
select: jest.fn().mockReturnThis(),
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
groupBy: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
getRawMany: jest.fn().mockResolvedValue(mockResult),
|
||||
});
|
||||
|
||||
const result = await service.getCommissionsByLevel(mockTenantId);
|
||||
|
||||
expect(result).toEqual([{ level: 1, count: 5, totalAmount: 500 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEarningsSummary', () => {
|
||||
it('should return earnings summary', async () => {
|
||||
const result = await service.getEarningsSummary(mockTenantId, mockNodeId);
|
||||
|
||||
expect(result.totalCommissions).toBeDefined();
|
||||
expect(result.totalBonuses).toBeDefined();
|
||||
expect(result.totalEarnings).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBonus', () => {
|
||||
it('should create a bonus', async () => {
|
||||
const mockBonus = {
|
||||
id: 'bonus-id',
|
||||
tenantId: mockTenantId,
|
||||
nodeId: mockNodeId,
|
||||
type: BonusType.RANK_ACHIEVEMENT,
|
||||
amount: 100,
|
||||
status: CommissionStatus.PENDING,
|
||||
};
|
||||
|
||||
bonusRepository.create.mockReturnValue(mockBonus as BonusEntity);
|
||||
bonusRepository.save.mockResolvedValue(mockBonus as BonusEntity);
|
||||
|
||||
const result = await service.createBonus(
|
||||
mockTenantId,
|
||||
mockNodeId,
|
||||
BonusType.RANK_ACHIEVEMENT,
|
||||
100,
|
||||
);
|
||||
|
||||
expect(bonusRepository.create).toHaveBeenCalled();
|
||||
expect(bonusRepository.save).toHaveBeenCalled();
|
||||
expect(nodeRepository.increment).toHaveBeenCalledWith({ id: mockNodeId }, 'totalEarnings', 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
337
src/modules/mlm/__tests__/nodes.controller.spec.ts
Normal file
337
src/modules/mlm/__tests__/nodes.controller.spec.ts
Normal file
@ -0,0 +1,337 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NodesController } from '../controllers/nodes.controller';
|
||||
import { NodesService } from '../services/nodes.service';
|
||||
import { CommissionsService } from '../services/commissions.service';
|
||||
import { CreateNodeDto, UpdateNodeDto, UpdateNodeStatusDto, NodeFiltersDto } from '../dto/node.dto';
|
||||
import { NodeStatus } from '../entities/node.entity';
|
||||
|
||||
describe('NodesController', () => {
|
||||
let controller: NodesController;
|
||||
let nodesService: jest.Mocked<NodesService>;
|
||||
let commissionsService: jest.Mocked<CommissionsService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockNodeId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
const mockStructureId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockNode = {
|
||||
id: mockNodeId,
|
||||
tenantId: mockTenantId,
|
||||
structureId: mockStructureId,
|
||||
userId: mockUserId,
|
||||
parentId: null,
|
||||
sponsorId: null,
|
||||
position: 1,
|
||||
depth: 0,
|
||||
path: 'root',
|
||||
inviteCode: 'ABC123',
|
||||
personalVolume: 1000,
|
||||
groupVolume: 5000,
|
||||
directReferrals: 5,
|
||||
totalDownline: 15,
|
||||
totalEarnings: 500,
|
||||
rankId: null,
|
||||
highestRankId: null,
|
||||
status: NodeStatus.ACTIVE,
|
||||
joinedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
structure: null as any,
|
||||
parent: null,
|
||||
sponsor: null,
|
||||
children: [],
|
||||
referrals: [],
|
||||
rank: null,
|
||||
highestRank: null,
|
||||
commissionsReceived: [],
|
||||
commissionsGenerated: [],
|
||||
bonuses: [],
|
||||
rankHistory: [],
|
||||
};
|
||||
|
||||
const mockNetworkSummary = {
|
||||
node: mockNode,
|
||||
currentRank: { id: 'rank-1', name: 'Gold', level: 3 },
|
||||
nextRank: { id: 'rank-2', name: 'Platinum', level: 4, progress: 0.75 },
|
||||
totalDownline: 15,
|
||||
directReferrals: 5,
|
||||
activeDownline: 10,
|
||||
personalVolume: 1000,
|
||||
groupVolume: 5000,
|
||||
totalEarnings: 500,
|
||||
};
|
||||
|
||||
const mockEarningsSummary = {
|
||||
totalCommissions: 500,
|
||||
totalBonuses: 100,
|
||||
totalEarnings: 600,
|
||||
pendingAmount: 50,
|
||||
paidAmount: 450,
|
||||
byLevel: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockNodesService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findByUserId: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
getDownline: jest.fn(),
|
||||
getUpline: jest.fn(),
|
||||
getTree: jest.fn(),
|
||||
getMyNetworkSummary: jest.fn(),
|
||||
generateInviteLink: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCommissionsService = {
|
||||
getEarningsSummary: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [NodesController],
|
||||
providers: [
|
||||
{ provide: NodesService, useValue: mockNodesService },
|
||||
{ provide: CommissionsService, useValue: mockCommissionsService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<NodesController>(NodesController);
|
||||
nodesService = module.get(NodesService);
|
||||
commissionsService = module.get(CommissionsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('CRUD Operations', () => {
|
||||
describe('create', () => {
|
||||
it('should create a new node', async () => {
|
||||
const createDto: CreateNodeDto = {
|
||||
structureId: mockStructureId,
|
||||
userId: 'new-user-id',
|
||||
};
|
||||
nodesService.create.mockResolvedValue({ ...mockNode, userId: 'new-user-id' });
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(nodesService.create).toHaveBeenCalledWith(mockTenantId, createDto);
|
||||
expect(result.userId).toBe('new-user-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all nodes', async () => {
|
||||
const filters: NodeFiltersDto = {};
|
||||
nodesService.findAll.mockResolvedValue({ items: [mockNode], total: 1 });
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(nodesService.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
expect(result.items).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should apply filters', async () => {
|
||||
const filters: NodeFiltersDto = { structureId: mockStructureId, status: NodeStatus.ACTIVE };
|
||||
nodesService.findAll.mockResolvedValue({ items: [mockNode], total: 1 });
|
||||
|
||||
await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(nodesService.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a node by id', async () => {
|
||||
nodesService.findOne.mockResolvedValue(mockNode);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockNodeId);
|
||||
|
||||
expect(nodesService.findOne).toHaveBeenCalledWith(mockTenantId, mockNodeId);
|
||||
expect(result).toEqual(mockNode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a node', async () => {
|
||||
const updateDto: UpdateNodeDto = { position: 2 };
|
||||
nodesService.update.mockResolvedValue({ ...mockNode, position: 2 });
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockNodeId, updateDto);
|
||||
|
||||
expect(nodesService.update).toHaveBeenCalledWith(mockTenantId, mockNodeId, updateDto);
|
||||
expect(result.position).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('should update node status', async () => {
|
||||
const statusDto: UpdateNodeStatusDto = { status: NodeStatus.INACTIVE };
|
||||
nodesService.updateStatus.mockResolvedValue({ ...mockNode, status: NodeStatus.INACTIVE });
|
||||
|
||||
const result = await controller.updateStatus(mockRequestUser, mockNodeId, statusDto);
|
||||
|
||||
expect(nodesService.updateStatus).toHaveBeenCalledWith(mockTenantId, mockNodeId, statusDto);
|
||||
expect(result.status).toBe(NodeStatus.INACTIVE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Network Navigation', () => {
|
||||
describe('getDownline', () => {
|
||||
it('should return downline nodes', async () => {
|
||||
nodesService.getDownline.mockResolvedValue([mockNode]);
|
||||
|
||||
const result = await controller.getDownline(mockRequestUser, mockNodeId);
|
||||
|
||||
expect(nodesService.getDownline).toHaveBeenCalledWith(mockTenantId, mockNodeId, undefined);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should accept maxDepth parameter', async () => {
|
||||
nodesService.getDownline.mockResolvedValue([mockNode]);
|
||||
|
||||
await controller.getDownline(mockRequestUser, mockNodeId, 5);
|
||||
|
||||
expect(nodesService.getDownline).toHaveBeenCalledWith(mockTenantId, mockNodeId, 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpline', () => {
|
||||
it('should return upline nodes', async () => {
|
||||
nodesService.getUpline.mockResolvedValue([mockNode]);
|
||||
|
||||
const result = await controller.getUpline(mockRequestUser, mockNodeId);
|
||||
|
||||
expect(nodesService.getUpline).toHaveBeenCalledWith(mockTenantId, mockNodeId);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTree', () => {
|
||||
it('should return tree structure', async () => {
|
||||
const mockTree = { id: mockNodeId, userId: mockUserId, depth: 0, position: 1, personalVolume: 1000, groupVolume: 5000, rankName: 'Gold', status: 'active', children: [] };
|
||||
nodesService.getTree.mockResolvedValue(mockTree as any);
|
||||
|
||||
const result = await controller.getTree(mockRequestUser, mockNodeId);
|
||||
|
||||
expect(nodesService.getTree).toHaveBeenCalledWith(mockTenantId, mockNodeId, 3);
|
||||
expect(result).toEqual(mockTree);
|
||||
});
|
||||
|
||||
it('should accept custom maxDepth', async () => {
|
||||
const mockTree = { id: mockNodeId, userId: mockUserId, depth: 0, position: 1, personalVolume: 1000, groupVolume: 5000, rankName: 'Gold', status: 'active', children: [] };
|
||||
nodesService.getTree.mockResolvedValue(mockTree as any);
|
||||
|
||||
await controller.getTree(mockRequestUser, mockNodeId, 5);
|
||||
|
||||
expect(nodesService.getTree).toHaveBeenCalledWith(mockTenantId, mockNodeId, 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('My Network', () => {
|
||||
describe('getMyDashboard', () => {
|
||||
it('should return current user dashboard', async () => {
|
||||
nodesService.getMyNetworkSummary.mockResolvedValue(mockNetworkSummary as any);
|
||||
|
||||
const result = await controller.getMyDashboard(mockRequestUser);
|
||||
|
||||
expect(nodesService.getMyNetworkSummary).toHaveBeenCalledWith(mockTenantId, mockUserId);
|
||||
expect(result).toEqual(mockNetworkSummary);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMyNetwork', () => {
|
||||
it('should return current user network tree', async () => {
|
||||
const mockTree = { id: mockNodeId, userId: mockUserId, depth: 0, position: 1, personalVolume: 1000, groupVolume: 5000, rankName: 'Gold', status: 'active', children: [] };
|
||||
nodesService.findByUserId.mockResolvedValue(mockNode as any);
|
||||
nodesService.getTree.mockResolvedValue(mockTree as any);
|
||||
|
||||
const result = await controller.getMyNetwork(mockRequestUser);
|
||||
|
||||
expect(nodesService.findByUserId).toHaveBeenCalledWith(mockTenantId, mockUserId);
|
||||
expect(nodesService.getTree).toHaveBeenCalledWith(mockTenantId, mockNodeId, 3);
|
||||
expect(result).toEqual(mockTree);
|
||||
});
|
||||
|
||||
it('should return null if user has no node', async () => {
|
||||
nodesService.findByUserId.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.getMyNetwork(mockRequestUser);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(nodesService.getTree).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMyEarnings', () => {
|
||||
it('should return current user earnings', async () => {
|
||||
nodesService.findByUserId.mockResolvedValue(mockNode as any);
|
||||
commissionsService.getEarningsSummary.mockResolvedValue(mockEarningsSummary as any);
|
||||
|
||||
const result = await controller.getMyEarnings(mockRequestUser);
|
||||
|
||||
expect(nodesService.findByUserId).toHaveBeenCalledWith(mockTenantId, mockUserId);
|
||||
expect(commissionsService.getEarningsSummary).toHaveBeenCalledWith(mockTenantId, mockNodeId);
|
||||
expect(result).toEqual(mockEarningsSummary);
|
||||
});
|
||||
|
||||
it('should return null if user has no node', async () => {
|
||||
nodesService.findByUserId.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.getMyEarnings(mockRequestUser);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(commissionsService.getEarningsSummary).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMyRank', () => {
|
||||
it('should return current user rank info', async () => {
|
||||
nodesService.getMyNetworkSummary.mockResolvedValue(mockNetworkSummary as any);
|
||||
|
||||
const result = await controller.getMyRank(mockRequestUser);
|
||||
|
||||
expect(nodesService.getMyNetworkSummary).toHaveBeenCalledWith(mockTenantId, mockUserId);
|
||||
expect(result).toEqual({
|
||||
currentRank: mockNetworkSummary.currentRank,
|
||||
nextRank: mockNetworkSummary.nextRank,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateInviteLink', () => {
|
||||
it('should generate invite link', async () => {
|
||||
const inviteLink = { inviteCode: 'XYZ789', inviteUrl: 'https://app.com/join/XYZ789' };
|
||||
nodesService.findByUserId.mockResolvedValue(mockNode as any);
|
||||
nodesService.generateInviteLink.mockResolvedValue(inviteLink);
|
||||
|
||||
const result = await controller.generateInviteLink(mockRequestUser);
|
||||
|
||||
expect(nodesService.findByUserId).toHaveBeenCalledWith(mockTenantId, mockUserId);
|
||||
expect(nodesService.generateInviteLink).toHaveBeenCalledWith(mockTenantId, mockNodeId);
|
||||
expect(result).toEqual(inviteLink);
|
||||
});
|
||||
|
||||
it('should return null if user has no node', async () => {
|
||||
nodesService.findByUserId.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.generateInviteLink(mockRequestUser);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(nodesService.generateInviteLink).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
300
src/modules/mlm/__tests__/nodes.service.spec.ts
Normal file
300
src/modules/mlm/__tests__/nodes.service.spec.ts
Normal file
@ -0,0 +1,300 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { NodesService } from '../services/nodes.service';
|
||||
import { NodeEntity, NodeStatus } from '../entities/node.entity';
|
||||
import { StructureEntity, StructureType } from '../entities/structure.entity';
|
||||
import { RankEntity } from '../entities/rank.entity';
|
||||
import { CreateNodeDto, NodeFiltersDto } from '../dto/node.dto';
|
||||
|
||||
describe('NodesService', () => {
|
||||
let service: NodesService;
|
||||
let nodeRepository: jest.Mocked<Repository<NodeEntity>>;
|
||||
let structureRepository: jest.Mocked<Repository<StructureEntity>>;
|
||||
let rankRepository: jest.Mocked<Repository<RankEntity>>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockStructureId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockNodeId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
|
||||
const mockStructure: StructureEntity = {
|
||||
id: mockStructureId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'Test Structure',
|
||||
description: null,
|
||||
type: StructureType.UNILEVEL,
|
||||
config: { maxDepth: 10 },
|
||||
levelRates: [{ level: 1, rate: 0.1 }],
|
||||
matchingRates: [],
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdBy: null,
|
||||
nodes: [],
|
||||
ranks: [],
|
||||
};
|
||||
|
||||
const mockNode = {
|
||||
id: mockNodeId,
|
||||
tenantId: mockTenantId,
|
||||
structureId: mockStructureId,
|
||||
userId: mockUserId,
|
||||
parentId: null,
|
||||
sponsorId: null,
|
||||
position: 1,
|
||||
depth: 0,
|
||||
path: mockNodeId.replace(/-/g, '_'),
|
||||
inviteCode: 'ABC123',
|
||||
personalVolume: 1000,
|
||||
groupVolume: 5000,
|
||||
directReferrals: 5,
|
||||
totalDownline: 15,
|
||||
totalEarnings: 500,
|
||||
rankId: null,
|
||||
highestRankId: null,
|
||||
status: NodeStatus.ACTIVE,
|
||||
joinedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
structure: null as any,
|
||||
parent: null,
|
||||
sponsor: null,
|
||||
children: [],
|
||||
referrals: [],
|
||||
rank: null as any,
|
||||
highestRank: null as any,
|
||||
commissionsReceived: [],
|
||||
commissionsGenerated: [],
|
||||
bonuses: [],
|
||||
rankHistory: [],
|
||||
} as NodeEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockNodeRepo = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findAndCount: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
count: jest.fn(),
|
||||
increment: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn().mockResolvedValue([]),
|
||||
})),
|
||||
};
|
||||
|
||||
const mockStructureRepo = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const mockRankRepo = {
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
NodesService,
|
||||
{ provide: getRepositoryToken(NodeEntity), useValue: mockNodeRepo },
|
||||
{ provide: getRepositoryToken(StructureEntity), useValue: mockStructureRepo },
|
||||
{ provide: getRepositoryToken(RankEntity), useValue: mockRankRepo },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<NodesService>(NodesService);
|
||||
nodeRepository = module.get(getRepositoryToken(NodeEntity));
|
||||
structureRepository = module.get(getRepositoryToken(StructureEntity));
|
||||
rankRepository = module.get(getRepositoryToken(RankEntity));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated nodes', async () => {
|
||||
nodeRepository.findAndCount.mockResolvedValue([[mockNode], 1]);
|
||||
|
||||
const result = await service.findAll(mockTenantId);
|
||||
|
||||
expect(nodeRepository.findAndCount).toHaveBeenCalled();
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should filter by structureId', async () => {
|
||||
const filters: NodeFiltersDto = { structureId: mockStructureId };
|
||||
nodeRepository.findAndCount.mockResolvedValue([[mockNode], 1]);
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(nodeRepository.findAndCount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ structureId: mockStructureId }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const filters: NodeFiltersDto = { status: NodeStatus.ACTIVE };
|
||||
nodeRepository.findAndCount.mockResolvedValue([[mockNode], 1]);
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(nodeRepository.findAndCount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ status: NodeStatus.ACTIVE }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a node by id', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(mockNode);
|
||||
|
||||
const result = await service.findOne(mockTenantId, mockNodeId);
|
||||
|
||||
expect(nodeRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: mockNodeId, tenantId: mockTenantId },
|
||||
relations: ['rank', 'highestRank'],
|
||||
});
|
||||
expect(result).toEqual(mockNode);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if node not found', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne(mockTenantId, 'non-existent')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByUserId', () => {
|
||||
it('should return node for user', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(mockNode);
|
||||
|
||||
const result = await service.findByUserId(mockTenantId, mockUserId);
|
||||
|
||||
expect(result).toEqual(mockNode);
|
||||
});
|
||||
|
||||
it('should filter by structureId if provided', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(mockNode);
|
||||
|
||||
await service.findByUserId(mockTenantId, mockUserId, mockStructureId);
|
||||
|
||||
expect(nodeRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { tenantId: mockTenantId, userId: mockUserId, structureId: mockStructureId },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new node', async () => {
|
||||
const createDto: CreateNodeDto = {
|
||||
structureId: mockStructureId,
|
||||
userId: 'new-user-id',
|
||||
};
|
||||
|
||||
structureRepository.findOne.mockResolvedValue(mockStructure);
|
||||
nodeRepository.findOne.mockResolvedValue(null); // No existing node
|
||||
nodeRepository.create.mockReturnValue({ ...mockNode, userId: 'new-user-id' });
|
||||
nodeRepository.save.mockResolvedValue({ ...mockNode, userId: 'new-user-id' });
|
||||
|
||||
const result = await service.create(mockTenantId, createDto);
|
||||
|
||||
expect(structureRepository.findOne).toHaveBeenCalled();
|
||||
expect(nodeRepository.create).toHaveBeenCalled();
|
||||
expect(result.userId).toBe('new-user-id');
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if structure not found', async () => {
|
||||
structureRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.create(mockTenantId, { structureId: 'invalid', userId: 'user' }),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if user already exists in structure', async () => {
|
||||
structureRepository.findOne.mockResolvedValue(mockStructure);
|
||||
nodeRepository.findOne.mockResolvedValue(mockNode); // User already exists
|
||||
|
||||
await expect(
|
||||
service.create(mockTenantId, { structureId: mockStructureId, userId: mockUserId }),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update node position', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(mockNode);
|
||||
nodeRepository.save.mockResolvedValue({ ...mockNode, position: 2 });
|
||||
|
||||
const result = await service.update(mockTenantId, mockNodeId, { position: 2 });
|
||||
|
||||
expect(result.position).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('should update node status', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(mockNode);
|
||||
nodeRepository.save.mockResolvedValue({ ...mockNode, status: NodeStatus.INACTIVE });
|
||||
|
||||
const result = await service.updateStatus(mockTenantId, mockNodeId, {
|
||||
status: NodeStatus.INACTIVE,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(NodeStatus.INACTIVE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDownline', () => {
|
||||
it('should return empty array if node has no path', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue({ ...mockNode, path: null });
|
||||
|
||||
const result = await service.getDownline(mockTenantId, mockNodeId);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateInviteLink', () => {
|
||||
it('should return existing invite code', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(mockNode);
|
||||
|
||||
const result = await service.generateInviteLink(mockTenantId, mockNodeId);
|
||||
|
||||
expect(result.inviteCode).toBe(mockNode.inviteCode);
|
||||
expect(result.inviteUrl).toContain(mockNode.inviteCode);
|
||||
});
|
||||
|
||||
it('should generate new invite code if none exists', async () => {
|
||||
const nodeWithoutCode = { ...mockNode, inviteCode: null };
|
||||
nodeRepository.findOne.mockResolvedValue(nodeWithoutCode);
|
||||
nodeRepository.save.mockResolvedValue({ ...nodeWithoutCode, inviteCode: 'NEWCODE123' });
|
||||
|
||||
const result = await service.generateInviteLink(mockTenantId, mockNodeId);
|
||||
|
||||
expect(nodeRepository.save).toHaveBeenCalled();
|
||||
expect(result.inviteCode).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMyNetworkSummary', () => {
|
||||
it('should throw NotFoundException if user has no node', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.getMyNetworkSummary(mockTenantId, 'unknown-user')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
135
src/modules/mlm/__tests__/ranks.controller.spec.ts
Normal file
135
src/modules/mlm/__tests__/ranks.controller.spec.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { RanksController } from '../controllers/ranks.controller';
|
||||
import { RanksService } from '../services/ranks.service';
|
||||
import { CreateRankDto, UpdateRankDto, RankFiltersDto } from '../dto/rank.dto';
|
||||
|
||||
describe('RanksController', () => {
|
||||
let controller: RanksController;
|
||||
let service: jest.Mocked<RanksService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockRankId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
const mockStructureId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockRank = {
|
||||
id: mockRankId,
|
||||
tenantId: mockTenantId,
|
||||
structureId: mockStructureId,
|
||||
name: 'Gold',
|
||||
level: 3,
|
||||
badgeUrl: null,
|
||||
color: '#FFD700',
|
||||
requirements: { personalVolume: 1000 },
|
||||
bonusRate: 0.05,
|
||||
benefits: {},
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
structure: null as any,
|
||||
currentNodes: [],
|
||||
highestRankNodes: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
evaluateRank: jest.fn(),
|
||||
evaluateAllNodes: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [RanksController],
|
||||
providers: [{ provide: RanksService, useValue: mockService }],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<RanksController>(RanksController);
|
||||
service = module.get(RanksService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all ranks', async () => {
|
||||
const filters: RankFiltersDto = {};
|
||||
service.findAll.mockResolvedValue([mockRank]);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
expect(result).toEqual([mockRank]);
|
||||
});
|
||||
|
||||
it('should filter by structureId', async () => {
|
||||
const filters: RankFiltersDto = { structureId: mockStructureId };
|
||||
service.findAll.mockResolvedValue([mockRank]);
|
||||
|
||||
await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a rank by id', async () => {
|
||||
service.findOne.mockResolvedValue(mockRank);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockRankId);
|
||||
|
||||
expect(service.findOne).toHaveBeenCalledWith(mockTenantId, mockRankId);
|
||||
expect(result).toEqual(mockRank);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new rank', async () => {
|
||||
const createDto: CreateRankDto = {
|
||||
structureId: mockStructureId,
|
||||
name: 'Platinum',
|
||||
level: 4,
|
||||
requirements: { personalVolume: 2000 },
|
||||
};
|
||||
service.create.mockResolvedValue({ ...mockRank, ...createDto });
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(service.create).toHaveBeenCalledWith(mockTenantId, createDto);
|
||||
expect(result.name).toBe('Platinum');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a rank', async () => {
|
||||
const updateDto: UpdateRankDto = { name: 'Updated Rank' };
|
||||
service.update.mockResolvedValue({ ...mockRank, name: 'Updated Rank' });
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockRankId, updateDto);
|
||||
|
||||
expect(service.update).toHaveBeenCalledWith(mockTenantId, mockRankId, updateDto);
|
||||
expect(result.name).toBe('Updated Rank');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove a rank', async () => {
|
||||
service.remove.mockResolvedValue(undefined);
|
||||
|
||||
await controller.remove(mockRequestUser, mockRankId);
|
||||
|
||||
expect(service.remove).toHaveBeenCalledWith(mockTenantId, mockRankId);
|
||||
});
|
||||
});
|
||||
});
|
||||
257
src/modules/mlm/__tests__/ranks.service.spec.ts
Normal file
257
src/modules/mlm/__tests__/ranks.service.spec.ts
Normal file
@ -0,0 +1,257 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { RanksService } from '../services/ranks.service';
|
||||
import { RankEntity } from '../entities/rank.entity';
|
||||
import { NodeEntity, NodeStatus } from '../entities/node.entity';
|
||||
import { CreateRankDto, UpdateRankDto, RankFiltersDto } from '../dto/rank.dto';
|
||||
|
||||
describe('RanksService', () => {
|
||||
let service: RanksService;
|
||||
let rankRepository: jest.Mocked<Repository<RankEntity>>;
|
||||
let nodeRepository: jest.Mocked<Repository<NodeEntity>>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockStructureId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockRankId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
const mockNodeId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
|
||||
const mockRank: RankEntity = {
|
||||
id: mockRankId,
|
||||
tenantId: mockTenantId,
|
||||
structureId: mockStructureId,
|
||||
name: 'Gold',
|
||||
level: 3,
|
||||
badgeUrl: 'https://example.com/gold.png',
|
||||
color: '#FFD700',
|
||||
requirements: {
|
||||
personalVolume: 1000,
|
||||
groupVolume: 10000,
|
||||
directReferrals: 5,
|
||||
},
|
||||
bonusRate: 0.05,
|
||||
benefits: { discount: 0.15 },
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
structure: null as any,
|
||||
currentNodes: [],
|
||||
highestRankNodes: [],
|
||||
};
|
||||
|
||||
const mockNode = {
|
||||
id: mockNodeId,
|
||||
tenantId: mockTenantId,
|
||||
structureId: mockStructureId,
|
||||
userId: 'user-001',
|
||||
parentId: null,
|
||||
sponsorId: null,
|
||||
position: 1,
|
||||
depth: 0,
|
||||
path: '/root',
|
||||
personalVolume: 1500,
|
||||
groupVolume: 15000,
|
||||
directReferrals: 8,
|
||||
totalDownline: 20,
|
||||
totalEarnings: 5000,
|
||||
rankId: null,
|
||||
highestRankId: null,
|
||||
status: NodeStatus.ACTIVE,
|
||||
inviteCode: 'ABC123',
|
||||
joinedAt: new Date('2026-01-01'),
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
structure: null as any,
|
||||
parent: null,
|
||||
sponsor: null,
|
||||
children: [],
|
||||
referrals: [],
|
||||
rank: null,
|
||||
highestRank: null,
|
||||
commissionsReceived: [],
|
||||
commissionsGenerated: [],
|
||||
bonuses: [],
|
||||
rankHistory: [],
|
||||
} as NodeEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockRankRepo = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
const mockNodeRepo = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
RanksService,
|
||||
{ provide: getRepositoryToken(RankEntity), useValue: mockRankRepo },
|
||||
{ provide: getRepositoryToken(NodeEntity), useValue: mockNodeRepo },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<RanksService>(RanksService);
|
||||
rankRepository = module.get(getRepositoryToken(RankEntity));
|
||||
nodeRepository = module.get(getRepositoryToken(NodeEntity));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all ranks for tenant', async () => {
|
||||
rankRepository.find.mockResolvedValue([mockRank]);
|
||||
|
||||
const result = await service.findAll(mockTenantId);
|
||||
|
||||
expect(rankRepository.find).toHaveBeenCalledWith({
|
||||
where: { tenantId: mockTenantId },
|
||||
order: { level: 'ASC' },
|
||||
});
|
||||
expect(result).toEqual([mockRank]);
|
||||
});
|
||||
|
||||
it('should filter by structureId', async () => {
|
||||
const filters: RankFiltersDto = { structureId: mockStructureId };
|
||||
rankRepository.find.mockResolvedValue([mockRank]);
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(rankRepository.find).toHaveBeenCalledWith({
|
||||
where: { tenantId: mockTenantId, structureId: mockStructureId },
|
||||
order: { level: 'ASC' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by isActive', async () => {
|
||||
const filters: RankFiltersDto = { isActive: true };
|
||||
rankRepository.find.mockResolvedValue([mockRank]);
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(rankRepository.find).toHaveBeenCalledWith({
|
||||
where: { tenantId: mockTenantId, isActive: true },
|
||||
order: { level: 'ASC' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a rank by id', async () => {
|
||||
rankRepository.findOne.mockResolvedValue(mockRank);
|
||||
|
||||
const result = await service.findOne(mockTenantId, mockRankId);
|
||||
|
||||
expect(rankRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: mockRankId, tenantId: mockTenantId },
|
||||
});
|
||||
expect(result).toEqual(mockRank);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if rank not found', async () => {
|
||||
rankRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne(mockTenantId, 'non-existent')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new rank', async () => {
|
||||
const createDto: CreateRankDto = {
|
||||
structureId: mockStructureId,
|
||||
name: 'Platinum',
|
||||
level: 4,
|
||||
requirements: { personalVolume: 2000 },
|
||||
};
|
||||
|
||||
rankRepository.create.mockReturnValue({ ...createDto, id: 'new-id', tenantId: mockTenantId } as RankEntity);
|
||||
rankRepository.save.mockResolvedValue({ ...createDto, id: 'new-id', tenantId: mockTenantId } as RankEntity);
|
||||
|
||||
const result = await service.create(mockTenantId, createDto);
|
||||
|
||||
expect(rankRepository.create).toHaveBeenCalledWith({ ...createDto, tenantId: mockTenantId });
|
||||
expect(result.name).toBe('Platinum');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update an existing rank', async () => {
|
||||
const updateDto: UpdateRankDto = { name: 'Updated Gold', level: 4 };
|
||||
rankRepository.findOne.mockResolvedValue(mockRank);
|
||||
rankRepository.save.mockResolvedValue({ ...mockRank, ...updateDto });
|
||||
|
||||
const result = await service.update(mockTenantId, mockRankId, updateDto);
|
||||
|
||||
expect(result.name).toBe('Updated Gold');
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if rank not found', async () => {
|
||||
rankRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.update(mockTenantId, 'non-existent', { name: 'New' })).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove a rank', async () => {
|
||||
rankRepository.findOne.mockResolvedValue(mockRank);
|
||||
rankRepository.remove.mockResolvedValue(mockRank);
|
||||
|
||||
await service.remove(mockTenantId, mockRankId);
|
||||
|
||||
expect(rankRepository.remove).toHaveBeenCalledWith(mockRank);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateRank', () => {
|
||||
it('should return qualified rank for node', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(mockNode);
|
||||
rankRepository.find.mockResolvedValue([mockRank]);
|
||||
|
||||
const result = await service.evaluateRank(mockTenantId, mockNodeId);
|
||||
|
||||
expect(result).toEqual(mockRank);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if node not found', async () => {
|
||||
nodeRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.evaluateRank(mockTenantId, 'non-existent')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should return null if node does not qualify for any rank', async () => {
|
||||
const lowVolumeNode = { ...mockNode, personalVolume: 100, groupVolume: 500, directReferrals: 1 };
|
||||
const highRequirementRank = { ...mockRank, requirements: { personalVolume: 5000 } };
|
||||
|
||||
nodeRepository.findOne.mockResolvedValue(lowVolumeNode);
|
||||
rankRepository.find.mockResolvedValue([highRequirementRank]);
|
||||
|
||||
const result = await service.evaluateRank(mockTenantId, mockNodeId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateAllNodes', () => {
|
||||
it('should evaluate and update all nodes in structure', async () => {
|
||||
nodeRepository.find.mockResolvedValue([mockNode]);
|
||||
nodeRepository.findOne.mockResolvedValue(mockNode);
|
||||
rankRepository.find.mockResolvedValue([mockRank]);
|
||||
nodeRepository.save.mockResolvedValue({ ...mockNode, rankId: mockRankId });
|
||||
|
||||
const result = await service.evaluateAllNodes(mockTenantId, mockStructureId);
|
||||
|
||||
expect(result).toBe(1);
|
||||
expect(nodeRepository.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
134
src/modules/mlm/__tests__/structures.controller.spec.ts
Normal file
134
src/modules/mlm/__tests__/structures.controller.spec.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { StructuresController } from '../controllers/structures.controller';
|
||||
import { StructuresService } from '../services/structures.service';
|
||||
import { StructureType } from '../entities/structure.entity';
|
||||
import { CreateStructureDto, UpdateStructureDto, StructureFiltersDto } from '../dto/structure.dto';
|
||||
|
||||
describe('StructuresController', () => {
|
||||
let controller: StructuresController;
|
||||
let service: jest.Mocked<StructuresService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockStructureId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockStructure = {
|
||||
id: mockStructureId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'Unilevel Network',
|
||||
description: 'Main compensation plan',
|
||||
type: StructureType.UNILEVEL,
|
||||
config: { maxDepth: 10, maxWidth: null },
|
||||
levelRates: [{ level: 1, rate: 0.1 }],
|
||||
matchingRates: [],
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
createdBy: mockUserId,
|
||||
nodes: [],
|
||||
ranks: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [StructuresController],
|
||||
providers: [
|
||||
{
|
||||
provide: StructuresService,
|
||||
useValue: mockService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<StructuresController>(StructuresController);
|
||||
service = module.get(StructuresService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all structures', async () => {
|
||||
const filters: StructureFiltersDto = {};
|
||||
service.findAll.mockResolvedValue([mockStructure]);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
expect(result).toEqual([mockStructure]);
|
||||
});
|
||||
|
||||
it('should filter by type', async () => {
|
||||
const filters: StructureFiltersDto = { type: StructureType.BINARY };
|
||||
service.findAll.mockResolvedValue([]);
|
||||
|
||||
await controller.findAll(mockRequestUser, filters);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockTenantId, filters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a structure by id', async () => {
|
||||
service.findOne.mockResolvedValue(mockStructure);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockStructureId);
|
||||
|
||||
expect(service.findOne).toHaveBeenCalledWith(mockTenantId, mockStructureId);
|
||||
expect(result).toEqual(mockStructure);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new structure', async () => {
|
||||
const createDto: CreateStructureDto = {
|
||||
name: 'Binary Network',
|
||||
type: StructureType.BINARY,
|
||||
};
|
||||
service.create.mockResolvedValue({ ...mockStructure, name: 'Binary Network' });
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(service.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result.name).toBe('Binary Network');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a structure', async () => {
|
||||
const updateDto: UpdateStructureDto = { name: 'Updated Name' };
|
||||
service.update.mockResolvedValue({ ...mockStructure, name: 'Updated Name' });
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockStructureId, updateDto);
|
||||
|
||||
expect(service.update).toHaveBeenCalledWith(mockTenantId, mockStructureId, updateDto);
|
||||
expect(result.name).toBe('Updated Name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove a structure', async () => {
|
||||
service.remove.mockResolvedValue(undefined);
|
||||
|
||||
await controller.remove(mockRequestUser, mockStructureId);
|
||||
|
||||
expect(service.remove).toHaveBeenCalledWith(mockTenantId, mockStructureId);
|
||||
});
|
||||
});
|
||||
});
|
||||
203
src/modules/mlm/__tests__/structures.service.spec.ts
Normal file
203
src/modules/mlm/__tests__/structures.service.spec.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository, ILike } from 'typeorm';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { StructuresService } from '../services/structures.service';
|
||||
import { StructureEntity, StructureType } from '../entities/structure.entity';
|
||||
import { CreateStructureDto, UpdateStructureDto, StructureFiltersDto } from '../dto/structure.dto';
|
||||
|
||||
describe('StructuresService', () => {
|
||||
let service: StructuresService;
|
||||
let repository: jest.Mocked<Repository<StructureEntity>>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockStructureId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockStructure: StructureEntity = {
|
||||
id: mockStructureId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'Unilevel Network',
|
||||
description: 'Main compensation plan',
|
||||
type: StructureType.UNILEVEL,
|
||||
config: { maxDepth: 10, maxWidth: null },
|
||||
levelRates: [
|
||||
{ level: 1, rate: 0.1 },
|
||||
{ level: 2, rate: 0.05 },
|
||||
],
|
||||
matchingRates: [],
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
createdBy: mockUserId,
|
||||
nodes: [],
|
||||
ranks: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockRepository = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
StructuresService,
|
||||
{
|
||||
provide: getRepositoryToken(StructureEntity),
|
||||
useValue: mockRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<StructuresService>(StructuresService);
|
||||
repository = module.get(getRepositoryToken(StructureEntity));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all structures for tenant', async () => {
|
||||
repository.find.mockResolvedValue([mockStructure]);
|
||||
|
||||
const result = await service.findAll(mockTenantId);
|
||||
|
||||
expect(repository.find).toHaveBeenCalledWith({
|
||||
where: { tenantId: mockTenantId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
expect(result).toEqual([mockStructure]);
|
||||
});
|
||||
|
||||
it('should filter by type', async () => {
|
||||
const filters: StructureFiltersDto = { type: StructureType.BINARY };
|
||||
repository.find.mockResolvedValue([]);
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(repository.find).toHaveBeenCalledWith({
|
||||
where: { tenantId: mockTenantId, type: StructureType.BINARY },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by isActive', async () => {
|
||||
const filters: StructureFiltersDto = { isActive: true };
|
||||
repository.find.mockResolvedValue([mockStructure]);
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(repository.find).toHaveBeenCalledWith({
|
||||
where: { tenantId: mockTenantId, isActive: true },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by search', async () => {
|
||||
const filters: StructureFiltersDto = { search: 'unilevel' };
|
||||
repository.find.mockResolvedValue([mockStructure]);
|
||||
|
||||
await service.findAll(mockTenantId, filters);
|
||||
|
||||
expect(repository.find).toHaveBeenCalledWith({
|
||||
where: { tenantId: mockTenantId, name: ILike('%unilevel%') },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a structure by id', async () => {
|
||||
repository.findOne.mockResolvedValue(mockStructure);
|
||||
|
||||
const result = await service.findOne(mockTenantId, mockStructureId);
|
||||
|
||||
expect(repository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: mockStructureId, tenantId: mockTenantId },
|
||||
});
|
||||
expect(result).toEqual(mockStructure);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if structure not found', async () => {
|
||||
repository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne(mockTenantId, 'non-existent-id')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new structure', async () => {
|
||||
const createDto: CreateStructureDto = {
|
||||
name: 'Binary Network',
|
||||
type: StructureType.BINARY,
|
||||
config: { spillover: 'balanced' },
|
||||
levelRates: [{ level: 1, rate: 0.15 }],
|
||||
};
|
||||
|
||||
const newStructure = { ...mockStructure, ...createDto, id: 'new-id' } as unknown as StructureEntity;
|
||||
repository.create.mockReturnValue(newStructure);
|
||||
repository.save.mockResolvedValue(newStructure);
|
||||
|
||||
const result = await service.create(mockTenantId, mockUserId, createDto);
|
||||
|
||||
expect(repository.create).toHaveBeenCalledWith({
|
||||
...createDto,
|
||||
tenantId: mockTenantId,
|
||||
createdBy: mockUserId,
|
||||
});
|
||||
expect(repository.save).toHaveBeenCalled();
|
||||
expect(result.name).toBe('Binary Network');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update an existing structure', async () => {
|
||||
const updateDto: UpdateStructureDto = {
|
||||
name: 'Updated Name',
|
||||
isActive: false,
|
||||
};
|
||||
repository.findOne.mockResolvedValue(mockStructure);
|
||||
repository.save.mockResolvedValue({ ...mockStructure, ...updateDto } as unknown as StructureEntity);
|
||||
|
||||
const result = await service.update(mockTenantId, mockStructureId, updateDto);
|
||||
|
||||
expect(repository.save).toHaveBeenCalled();
|
||||
expect(result.name).toBe('Updated Name');
|
||||
expect(result.isActive).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if structure not found', async () => {
|
||||
repository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.update(mockTenantId, 'non-existent-id', { name: 'New Name' }),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove a structure', async () => {
|
||||
repository.findOne.mockResolvedValue(mockStructure);
|
||||
repository.remove.mockResolvedValue(mockStructure);
|
||||
|
||||
await service.remove(mockTenantId, mockStructureId);
|
||||
|
||||
expect(repository.remove).toHaveBeenCalledWith(mockStructure);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if structure not found', async () => {
|
||||
repository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.remove(mockTenantId, 'non-existent-id')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -8,14 +8,9 @@ import {
|
||||
} from 'typeorm';
|
||||
import { NodeEntity } from './node.entity';
|
||||
import { RankEntity } from './rank.entity';
|
||||
import { CommissionStatus } from './commission.entity';
|
||||
import { BonusType, CommissionStatus } from './enums';
|
||||
|
||||
export enum BonusType {
|
||||
RANK_ACHIEVEMENT = 'rank_achievement',
|
||||
RANK_MAINTENANCE = 'rank_maintenance',
|
||||
FAST_START = 'fast_start',
|
||||
POOL_SHARE = 'pool_share',
|
||||
}
|
||||
export { BonusType };
|
||||
|
||||
@Entity({ schema: 'mlm', name: 'bonuses' })
|
||||
export class BonusEntity {
|
||||
|
||||
@ -7,21 +7,9 @@ import {
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { NodeEntity } from './node.entity';
|
||||
import { CommissionType, CommissionStatus } from './enums';
|
||||
|
||||
export enum CommissionType {
|
||||
LEVEL = 'level',
|
||||
MATCHING = 'matching',
|
||||
INFINITY = 'infinity',
|
||||
LEADERSHIP = 'leadership',
|
||||
POOL = 'pool',
|
||||
}
|
||||
|
||||
export enum CommissionStatus {
|
||||
PENDING = 'pending',
|
||||
APPROVED = 'approved',
|
||||
PAID = 'paid',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
export { CommissionType, CommissionStatus };
|
||||
|
||||
@Entity({ schema: 'mlm', name: 'commissions' })
|
||||
export class CommissionEntity {
|
||||
|
||||
37
src/modules/mlm/entities/enums.ts
Normal file
37
src/modules/mlm/entities/enums.ts
Normal file
@ -0,0 +1,37 @@
|
||||
// MLM Module Enums - Extracted to avoid circular dependencies
|
||||
|
||||
export enum NodeStatus {
|
||||
PENDING = 'pending',
|
||||
ACTIVE = 'active',
|
||||
INACTIVE = 'inactive',
|
||||
SUSPENDED = 'suspended',
|
||||
}
|
||||
|
||||
export enum CommissionType {
|
||||
LEVEL = 'level',
|
||||
MATCHING = 'matching',
|
||||
INFINITY = 'infinity',
|
||||
LEADERSHIP = 'leadership',
|
||||
POOL = 'pool',
|
||||
}
|
||||
|
||||
export enum CommissionStatus {
|
||||
PENDING = 'pending',
|
||||
APPROVED = 'approved',
|
||||
PAID = 'paid',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
|
||||
export enum BonusType {
|
||||
RANK_ACHIEVEMENT = 'rank_achievement',
|
||||
RANK_MAINTENANCE = 'rank_maintenance',
|
||||
FAST_START = 'fast_start',
|
||||
POOL_SHARE = 'pool_share',
|
||||
}
|
||||
|
||||
export enum StructureType {
|
||||
UNILEVEL = 'unilevel',
|
||||
BINARY = 'binary',
|
||||
MATRIX = 'matrix',
|
||||
HYBRID = 'hybrid',
|
||||
}
|
||||
@ -13,13 +13,9 @@ import { RankEntity } from './rank.entity';
|
||||
import { CommissionEntity } from './commission.entity';
|
||||
import { BonusEntity } from './bonus.entity';
|
||||
import { RankHistoryEntity } from './rank-history.entity';
|
||||
import { NodeStatus } from './enums';
|
||||
|
||||
export enum NodeStatus {
|
||||
PENDING = 'pending',
|
||||
ACTIVE = 'active',
|
||||
INACTIVE = 'inactive',
|
||||
SUSPENDED = 'suspended',
|
||||
}
|
||||
export { NodeStatus };
|
||||
|
||||
@Entity({ schema: 'mlm', name: 'nodes' })
|
||||
export class NodeEntity {
|
||||
|
||||
@ -8,13 +8,9 @@ import {
|
||||
} from 'typeorm';
|
||||
import { NodeEntity } from './node.entity';
|
||||
import { RankEntity } from './rank.entity';
|
||||
import { StructureType } from './enums';
|
||||
|
||||
export enum StructureType {
|
||||
UNILEVEL = 'unilevel',
|
||||
BINARY = 'binary',
|
||||
MATRIX = 'matrix',
|
||||
HYBRID = 'hybrid',
|
||||
}
|
||||
export { StructureType };
|
||||
|
||||
export interface UnilevelConfig {
|
||||
maxWidth?: number | null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user