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:
Adrian Flores Cortes 2026-02-03 16:35:01 -06:00
parent 23bd170769
commit a671697d35
17 changed files with 3145 additions and 33 deletions

View 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);
});
});
});
});

View 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);
});
});
});

View 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);
});
});
});

View 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);
});
});
});

View 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);
});
});
});

View 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);
});
});
});

View 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();
});
});
});
});

View 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,
);
});
});
});

View 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);
});
});
});

View 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();
});
});
});

View 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);
});
});
});

View 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,
);
});
});
});

View File

@ -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 {

View File

@ -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 {

View 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',
}

View File

@ -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 {

View File

@ -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;