test(P2): Add controller tests for sales, commissions, and portfolio
- Add 12 controller test files (117 tests total): - sales: leads, opportunities, activities, pipeline, dashboard - commissions: schemes, assignments, entries, periods, dashboard - portfolio: categories, products - Fix flaky test in assignments.service.spec.ts (timestamp issue) - All 441 tests passing for these modules Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b1ee86e0e5
commit
61d7d29212
185
src/modules/commissions/__tests__/assignments.controller.spec.ts
Normal file
185
src/modules/commissions/__tests__/assignments.controller.spec.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AssignmentsController } from '../controllers/assignments.controller';
|
||||
import { AssignmentsService } from '../services/assignments.service';
|
||||
import { CreateAssignmentDto, UpdateAssignmentDto, AssignmentListQueryDto } from '../dto';
|
||||
|
||||
describe('AssignmentsController', () => {
|
||||
let controller: AssignmentsController;
|
||||
let assignmentsService: 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 mockSchemeId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockAssignment = {
|
||||
id: mockAssignmentId,
|
||||
tenantId: mockTenantId,
|
||||
userId: mockUserId,
|
||||
schemeId: mockSchemeId,
|
||||
isActive: true,
|
||||
customRate: null,
|
||||
effectiveFrom: new Date('2026-01-01'),
|
||||
effectiveTo: null,
|
||||
notes: 'Standard assignment',
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
createdBy: mockUserId,
|
||||
};
|
||||
|
||||
const mockPaginatedAssignments = {
|
||||
data: [mockAssignment],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockAssignmentsService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findActiveForUser: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AssignmentsController],
|
||||
providers: [
|
||||
{
|
||||
provide: AssignmentsService,
|
||||
useValue: mockAssignmentsService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<AssignmentsController>(AssignmentsController);
|
||||
assignmentsService = module.get(AssignmentsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated assignments', async () => {
|
||||
const query: AssignmentListQueryDto = { page: 1, limit: 20 };
|
||||
assignmentsService.findAll.mockResolvedValue(mockPaginatedAssignments);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(assignmentsService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
expect(result).toEqual(mockPaginatedAssignments);
|
||||
});
|
||||
|
||||
it('should filter by userId', async () => {
|
||||
const query: AssignmentListQueryDto = { page: 1, limit: 20, userId: mockUserId };
|
||||
assignmentsService.findAll.mockResolvedValue(mockPaginatedAssignments);
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(assignmentsService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findMyAssignments', () => {
|
||||
it('should return current user active assignments', async () => {
|
||||
assignmentsService.findActiveForUser.mockResolvedValue([mockAssignment]);
|
||||
|
||||
const result = await controller.findMyAssignments(mockRequestUser);
|
||||
|
||||
expect(assignmentsService.findActiveForUser).toHaveBeenCalledWith(mockTenantId, mockUserId);
|
||||
expect(result).toEqual([mockAssignment]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return an assignment by id', async () => {
|
||||
assignmentsService.findOne.mockResolvedValue(mockAssignment);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockAssignmentId);
|
||||
|
||||
expect(assignmentsService.findOne).toHaveBeenCalledWith(mockTenantId, mockAssignmentId);
|
||||
expect(result).toEqual(mockAssignment);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new assignment', async () => {
|
||||
const createDto: CreateAssignmentDto = {
|
||||
userId: 'another-user-id',
|
||||
schemeId: mockSchemeId,
|
||||
effectiveFrom: '2026-02-01',
|
||||
};
|
||||
const createdAssignment = { ...mockAssignment, ...createDto, id: 'new-assignment-id' };
|
||||
assignmentsService.create.mockResolvedValue(createdAssignment);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(assignmentsService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result).toEqual(createdAssignment);
|
||||
});
|
||||
|
||||
it('should create assignment with custom rate', async () => {
|
||||
const createDto: CreateAssignmentDto = {
|
||||
userId: mockUserId,
|
||||
schemeId: mockSchemeId,
|
||||
customRate: 12.5,
|
||||
};
|
||||
assignmentsService.create.mockResolvedValue({ ...mockAssignment, customRate: 12.5 });
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(result.customRate).toBe(12.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update an assignment', async () => {
|
||||
const updateDto: UpdateAssignmentDto = {
|
||||
customRate: 15,
|
||||
notes: 'Updated notes',
|
||||
};
|
||||
const updatedAssignment = { ...mockAssignment, ...updateDto };
|
||||
assignmentsService.update.mockResolvedValue(updatedAssignment);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockAssignmentId, updateDto);
|
||||
|
||||
expect(assignmentsService.update).toHaveBeenCalledWith(mockTenantId, mockAssignmentId, updateDto);
|
||||
expect(result.customRate).toBe(15);
|
||||
});
|
||||
|
||||
it('should deactivate an assignment', async () => {
|
||||
const updateDto: UpdateAssignmentDto = {
|
||||
isActive: false,
|
||||
effectiveTo: '2026-03-01',
|
||||
};
|
||||
const deactivatedAssignment = { ...mockAssignment, isActive: false };
|
||||
assignmentsService.update.mockResolvedValue(deactivatedAssignment);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockAssignmentId, updateDto);
|
||||
|
||||
expect(result.isActive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should delete an assignment', async () => {
|
||||
assignmentsService.remove.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.remove(mockRequestUser, mockAssignmentId);
|
||||
|
||||
expect(assignmentsService.remove).toHaveBeenCalledWith(mockTenantId, mockAssignmentId);
|
||||
expect(result).toEqual({ message: 'Commission assignment deleted successfully' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -380,12 +380,13 @@ describe('AssignmentsService', () => {
|
||||
|
||||
describe('remove', () => {
|
||||
it('should delete an assignment', async () => {
|
||||
assignmentRepo.findOne.mockResolvedValue(createMockAssignment() as CommissionAssignmentEntity);
|
||||
assignmentRepo.remove.mockResolvedValue(createMockAssignment() as CommissionAssignmentEntity);
|
||||
const mockAssignment = createMockAssignment() as CommissionAssignmentEntity;
|
||||
assignmentRepo.findOne.mockResolvedValue(mockAssignment);
|
||||
assignmentRepo.remove.mockResolvedValue(mockAssignment);
|
||||
|
||||
await service.remove(mockTenantId, mockAssignmentId);
|
||||
|
||||
expect(assignmentRepo.remove).toHaveBeenCalledWith(createMockAssignment());
|
||||
expect(assignmentRepo.remove).toHaveBeenCalledWith(mockAssignment);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when assignment not found', async () => {
|
||||
|
||||
177
src/modules/commissions/__tests__/dashboard.controller.spec.ts
Normal file
177
src/modules/commissions/__tests__/dashboard.controller.spec.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CommissionsDashboardController } from '../controllers/dashboard.controller';
|
||||
import { CommissionsDashboardService } from '../services/commissions-dashboard.service';
|
||||
import { EntryStatus } from '../entities/commission-entry.entity';
|
||||
|
||||
describe('CommissionsDashboardController', () => {
|
||||
let controller: CommissionsDashboardController;
|
||||
let dashboardService: jest.Mocked<CommissionsDashboardService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockDashboardSummary = {
|
||||
totalSchemes: 5,
|
||||
activeSchemes: 4,
|
||||
totalAssignments: 25,
|
||||
activeAssignments: 20,
|
||||
totalEntries: 150,
|
||||
pendingEntries: 30,
|
||||
approvedEntries: 100,
|
||||
paidEntries: 20,
|
||||
totalPendingAmount: 15000,
|
||||
totalApprovedAmount: 50000,
|
||||
totalPaidAmount: 10000,
|
||||
currentPeriodId: 'period-123',
|
||||
currentPeriodName: 'January 2026',
|
||||
};
|
||||
|
||||
const mockUserEarnings = {
|
||||
userId: mockUserId,
|
||||
userName: 'John Doe',
|
||||
totalEarnings: 25000,
|
||||
pendingAmount: 5000,
|
||||
approvedAmount: 15000,
|
||||
paidAmount: 5000,
|
||||
totalEntries: 50,
|
||||
averageCommission: 500,
|
||||
lastEntryDate: new Date('2026-01-15'),
|
||||
};
|
||||
|
||||
const mockEntriesByStatus = [
|
||||
{ status: EntryStatus.PENDING, count: 30, amount: 15000 },
|
||||
{ status: EntryStatus.APPROVED, count: 100, amount: 50000 },
|
||||
{ status: EntryStatus.PAID, count: 20, amount: 10000 },
|
||||
];
|
||||
|
||||
const mockEntriesByScheme = [
|
||||
{ schemeId: 'scheme-1', schemeName: 'Sales Commission', count: 80, amount: 40000 },
|
||||
{ schemeId: 'scheme-2', schemeName: 'Referral Bonus', count: 50, amount: 25000 },
|
||||
{ schemeId: 'scheme-3', schemeName: 'Performance Bonus', count: 20, amount: 10000 },
|
||||
];
|
||||
|
||||
const mockEntriesByUser = [
|
||||
{ userId: mockUserId, userName: 'John Doe', count: 50, amount: 25000 },
|
||||
{ userId: 'user-2', userName: 'Jane Smith', count: 40, amount: 20000 },
|
||||
{ userId: 'user-3', userName: 'Bob Johnson', count: 30, amount: 15000 },
|
||||
];
|
||||
|
||||
const mockTopEarners = [
|
||||
{ userId: mockUserId, userName: 'John Doe', count: 50, amount: 25000 },
|
||||
{ userId: 'user-2', userName: 'Jane Smith', count: 40, amount: 20000 },
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockDashboardService = {
|
||||
getDashboardSummary: jest.fn(),
|
||||
getUserEarnings: jest.fn(),
|
||||
getEntriesByStatus: jest.fn(),
|
||||
getEntriesByScheme: jest.fn(),
|
||||
getEntriesByUser: jest.fn(),
|
||||
getTopEarners: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [CommissionsDashboardController],
|
||||
providers: [
|
||||
{
|
||||
provide: CommissionsDashboardService,
|
||||
useValue: mockDashboardService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<CommissionsDashboardController>(CommissionsDashboardController);
|
||||
dashboardService = module.get(CommissionsDashboardService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getSummary', () => {
|
||||
it('should return dashboard summary', async () => {
|
||||
dashboardService.getDashboardSummary.mockResolvedValue(mockDashboardSummary);
|
||||
|
||||
const result = await controller.getSummary(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getDashboardSummary).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockDashboardSummary);
|
||||
expect(result.totalSchemes).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMyEarnings', () => {
|
||||
it('should return current user earnings', async () => {
|
||||
dashboardService.getUserEarnings.mockResolvedValue(mockUserEarnings);
|
||||
|
||||
const result = await controller.getMyEarnings(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getUserEarnings).toHaveBeenCalledWith(mockTenantId, mockUserId);
|
||||
expect(result).toEqual(mockUserEarnings);
|
||||
expect(result.totalEarnings).toBe(25000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntriesByStatus', () => {
|
||||
it('should return entries grouped by status', async () => {
|
||||
dashboardService.getEntriesByStatus.mockResolvedValue(mockEntriesByStatus);
|
||||
|
||||
const result = await controller.getEntriesByStatus(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getEntriesByStatus).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockEntriesByStatus);
|
||||
expect(result.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntriesByScheme', () => {
|
||||
it('should return entries grouped by scheme', async () => {
|
||||
dashboardService.getEntriesByScheme.mockResolvedValue(mockEntriesByScheme);
|
||||
|
||||
const result = await controller.getEntriesByScheme(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getEntriesByScheme).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockEntriesByScheme);
|
||||
expect(result.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntriesByUser', () => {
|
||||
it('should return entries grouped by user', async () => {
|
||||
dashboardService.getEntriesByUser.mockResolvedValue(mockEntriesByUser);
|
||||
|
||||
const result = await controller.getEntriesByUser(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getEntriesByUser).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockEntriesByUser);
|
||||
expect(result.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTopEarners', () => {
|
||||
it('should return top earners with default limit', async () => {
|
||||
dashboardService.getTopEarners.mockResolvedValue(mockTopEarners);
|
||||
|
||||
const result = await controller.getTopEarners(mockRequestUser, undefined);
|
||||
|
||||
expect(dashboardService.getTopEarners).toHaveBeenCalledWith(mockTenantId, 10);
|
||||
expect(result).toEqual(mockTopEarners);
|
||||
});
|
||||
|
||||
it('should return top earners with custom limit', async () => {
|
||||
dashboardService.getTopEarners.mockResolvedValue(mockTopEarners.slice(0, 1));
|
||||
|
||||
const result = await controller.getTopEarners(mockRequestUser, 5);
|
||||
|
||||
expect(dashboardService.getTopEarners).toHaveBeenCalledWith(mockTenantId, 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
243
src/modules/commissions/__tests__/entries.controller.spec.ts
Normal file
243
src/modules/commissions/__tests__/entries.controller.spec.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { EntriesController } from '../controllers/entries.controller';
|
||||
import { EntriesService } from '../services/entries.service';
|
||||
import {
|
||||
CreateEntryDto,
|
||||
UpdateEntryDto,
|
||||
ApproveEntryDto,
|
||||
RejectEntryDto,
|
||||
EntryListQueryDto,
|
||||
CalculateCommissionDto,
|
||||
} from '../dto';
|
||||
import { EntryStatus } from '../entities/commission-entry.entity';
|
||||
|
||||
describe('EntriesController', () => {
|
||||
let controller: EntriesController;
|
||||
let entriesService: jest.Mocked<EntriesService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockEntryId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
const mockSchemeId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockEntry = {
|
||||
id: mockEntryId,
|
||||
tenantId: mockTenantId,
|
||||
userId: mockUserId,
|
||||
schemeId: mockSchemeId,
|
||||
referenceType: 'opportunity',
|
||||
referenceId: 'opp-123',
|
||||
baseAmount: 10000,
|
||||
rateApplied: 10,
|
||||
commissionAmount: 1000,
|
||||
currency: 'USD',
|
||||
status: EntryStatus.PENDING,
|
||||
periodId: null,
|
||||
notes: 'Commission for closed deal',
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
};
|
||||
|
||||
const mockPaginatedEntries = {
|
||||
data: [mockEntry],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockEntriesService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
approve: jest.fn(),
|
||||
reject: jest.fn(),
|
||||
calculateCommission: jest.fn(),
|
||||
bulkApprove: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [EntriesController],
|
||||
providers: [
|
||||
{
|
||||
provide: EntriesService,
|
||||
useValue: mockEntriesService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<EntriesController>(EntriesController);
|
||||
entriesService = module.get(EntriesService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated entries', async () => {
|
||||
const query: EntryListQueryDto = { page: 1, limit: 20 };
|
||||
entriesService.findAll.mockResolvedValue(mockPaginatedEntries);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(entriesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
expect(result).toEqual(mockPaginatedEntries);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const query: EntryListQueryDto = { page: 1, limit: 20, status: EntryStatus.APPROVED };
|
||||
entriesService.findAll.mockResolvedValue({ ...mockPaginatedEntries, data: [] });
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(entriesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findMyEntries', () => {
|
||||
it('should return current user entries', async () => {
|
||||
const query: EntryListQueryDto = { page: 1, limit: 20 };
|
||||
entriesService.findAll.mockResolvedValue(mockPaginatedEntries);
|
||||
|
||||
const result = await controller.findMyEntries(mockRequestUser, query);
|
||||
|
||||
expect(entriesService.findAll).toHaveBeenCalledWith(mockTenantId, { ...query, userId: mockUserId });
|
||||
expect(result).toEqual(mockPaginatedEntries);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return an entry by id', async () => {
|
||||
entriesService.findOne.mockResolvedValue(mockEntry);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockEntryId);
|
||||
|
||||
expect(entriesService.findOne).toHaveBeenCalledWith(mockTenantId, mockEntryId);
|
||||
expect(result).toEqual(mockEntry);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new entry', async () => {
|
||||
const createDto: CreateEntryDto = {
|
||||
userId: mockUserId,
|
||||
schemeId: mockSchemeId,
|
||||
referenceType: 'opportunity',
|
||||
referenceId: 'new-opp-id',
|
||||
baseAmount: 5000,
|
||||
};
|
||||
const createdEntry = { ...mockEntry, ...createDto, id: 'new-entry-id', commissionAmount: 500 };
|
||||
entriesService.create.mockResolvedValue(createdEntry);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(entriesService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result).toEqual(createdEntry);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update an entry', async () => {
|
||||
const updateDto: UpdateEntryDto = {
|
||||
notes: 'Updated notes',
|
||||
};
|
||||
const updatedEntry = { ...mockEntry, ...updateDto };
|
||||
entriesService.update.mockResolvedValue(updatedEntry);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockEntryId, updateDto);
|
||||
|
||||
expect(entriesService.update).toHaveBeenCalledWith(mockTenantId, mockEntryId, updateDto);
|
||||
expect(result.notes).toBe('Updated notes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('approve', () => {
|
||||
it('should approve an entry', async () => {
|
||||
const approveDto: ApproveEntryDto = {
|
||||
notes: 'Approved by manager',
|
||||
};
|
||||
const approvedEntry = {
|
||||
...mockEntry,
|
||||
status: EntryStatus.APPROVED,
|
||||
approvedBy: mockUserId,
|
||||
approvedAt: new Date(),
|
||||
};
|
||||
entriesService.approve.mockResolvedValue(approvedEntry);
|
||||
|
||||
const result = await controller.approve(mockRequestUser, mockEntryId, approveDto);
|
||||
|
||||
expect(entriesService.approve).toHaveBeenCalledWith(mockTenantId, mockEntryId, mockUserId, approveDto);
|
||||
expect(result.status).toBe(EntryStatus.APPROVED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reject', () => {
|
||||
it('should reject an entry', async () => {
|
||||
const rejectDto: RejectEntryDto = {
|
||||
reason: 'Invalid reference',
|
||||
};
|
||||
const rejectedEntry = {
|
||||
...mockEntry,
|
||||
status: EntryStatus.REJECTED,
|
||||
};
|
||||
entriesService.reject.mockResolvedValue(rejectedEntry);
|
||||
|
||||
const result = await controller.reject(mockRequestUser, mockEntryId, rejectDto);
|
||||
|
||||
expect(entriesService.reject).toHaveBeenCalledWith(mockTenantId, mockEntryId, mockUserId, rejectDto);
|
||||
expect(result.status).toBe(EntryStatus.REJECTED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateCommission', () => {
|
||||
it('should calculate commission', async () => {
|
||||
const calculateDto: CalculateCommissionDto = {
|
||||
schemeId: mockSchemeId,
|
||||
baseAmount: 10000,
|
||||
};
|
||||
const calculationResult = {
|
||||
baseAmount: 10000,
|
||||
rate: 10,
|
||||
commissionAmount: 1000,
|
||||
currency: 'USD',
|
||||
};
|
||||
entriesService.calculateCommission.mockResolvedValue(calculationResult);
|
||||
|
||||
const result = await controller.calculateCommission(mockRequestUser, calculateDto);
|
||||
|
||||
expect(entriesService.calculateCommission).toHaveBeenCalledWith(mockTenantId, calculateDto);
|
||||
expect(result.commissionAmount).toBe(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkApprove', () => {
|
||||
it('should bulk approve entries', async () => {
|
||||
const entryIds = [mockEntryId, 'entry-2', 'entry-3'];
|
||||
entriesService.bulkApprove.mockResolvedValue(3);
|
||||
|
||||
const result = await controller.bulkApprove(mockRequestUser, { entryIds });
|
||||
|
||||
expect(entriesService.bulkApprove).toHaveBeenCalledWith(mockTenantId, entryIds, mockUserId);
|
||||
expect(result).toEqual({ approved: 3 });
|
||||
});
|
||||
|
||||
it('should return count of approved entries', async () => {
|
||||
const entryIds = [mockEntryId, 'entry-2'];
|
||||
entriesService.bulkApprove.mockResolvedValue(1); // Only 1 was valid
|
||||
|
||||
const result = await controller.bulkApprove(mockRequestUser, { entryIds });
|
||||
|
||||
expect(result.approved).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
235
src/modules/commissions/__tests__/periods.controller.spec.ts
Normal file
235
src/modules/commissions/__tests__/periods.controller.spec.ts
Normal file
@ -0,0 +1,235 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PeriodsController } from '../controllers/periods.controller';
|
||||
import { PeriodsService } from '../services/periods.service';
|
||||
import { CreatePeriodDto, UpdatePeriodDto, ClosePeriodDto, MarkPaidDto, PeriodListQueryDto } from '../dto';
|
||||
import { PeriodStatus } from '../entities/commission-period.entity';
|
||||
|
||||
describe('PeriodsController', () => {
|
||||
let controller: PeriodsController;
|
||||
let periodsService: jest.Mocked<PeriodsService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockPeriodId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockPeriod = {
|
||||
id: mockPeriodId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'January 2026',
|
||||
startsAt: new Date('2026-01-01'),
|
||||
endsAt: new Date('2026-01-31'),
|
||||
totalEntries: 10,
|
||||
totalAmount: 5000,
|
||||
currency: 'USD',
|
||||
status: PeriodStatus.OPEN,
|
||||
closedAt: null,
|
||||
closedBy: null,
|
||||
paidAt: null,
|
||||
paidBy: null,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
createdBy: mockUserId,
|
||||
};
|
||||
|
||||
const mockPaginatedPeriods = {
|
||||
data: [mockPeriod],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockPeriodsService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
getCurrentPeriod: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
close: jest.fn(),
|
||||
markAsPaid: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
assignEntriesToPeriod: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PeriodsController],
|
||||
providers: [
|
||||
{
|
||||
provide: PeriodsService,
|
||||
useValue: mockPeriodsService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PeriodsController>(PeriodsController);
|
||||
periodsService = module.get(PeriodsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated periods', async () => {
|
||||
const query: PeriodListQueryDto = { page: 1, limit: 20 };
|
||||
periodsService.findAll.mockResolvedValue(mockPaginatedPeriods);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(periodsService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
expect(result).toEqual(mockPaginatedPeriods);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const query: PeriodListQueryDto = { page: 1, limit: 20, status: PeriodStatus.CLOSED };
|
||||
periodsService.findAll.mockResolvedValue({ ...mockPaginatedPeriods, data: [] });
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(periodsService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentPeriod', () => {
|
||||
it('should return the current open period', async () => {
|
||||
periodsService.getCurrentPeriod.mockResolvedValue(mockPeriod);
|
||||
|
||||
const result = await controller.getCurrentPeriod(mockRequestUser);
|
||||
|
||||
expect(periodsService.getCurrentPeriod).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockPeriod);
|
||||
});
|
||||
|
||||
it('should return null when no current period exists', async () => {
|
||||
periodsService.getCurrentPeriod.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.getCurrentPeriod(mockRequestUser);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a period by id', async () => {
|
||||
periodsService.findOne.mockResolvedValue(mockPeriod);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockPeriodId);
|
||||
|
||||
expect(periodsService.findOne).toHaveBeenCalledWith(mockTenantId, mockPeriodId);
|
||||
expect(result).toEqual(mockPeriod);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new period', async () => {
|
||||
const createDto: CreatePeriodDto = {
|
||||
name: 'February 2026',
|
||||
startsAt: '2026-02-01',
|
||||
endsAt: '2026-02-28',
|
||||
};
|
||||
const createdPeriod = { ...mockPeriod, ...createDto, id: 'new-period-id' };
|
||||
periodsService.create.mockResolvedValue(createdPeriod);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(periodsService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result).toEqual(createdPeriod);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a period', async () => {
|
||||
const updateDto: UpdatePeriodDto = {
|
||||
name: 'Updated Period Name',
|
||||
};
|
||||
const updatedPeriod = { ...mockPeriod, ...updateDto };
|
||||
periodsService.update.mockResolvedValue(updatedPeriod);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockPeriodId, updateDto);
|
||||
|
||||
expect(periodsService.update).toHaveBeenCalledWith(mockTenantId, mockPeriodId, updateDto);
|
||||
expect(result.name).toBe('Updated Period Name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
it('should close a period', async () => {
|
||||
const closeDto: ClosePeriodDto = {
|
||||
notes: 'Period closed for payroll',
|
||||
};
|
||||
const closedPeriod = {
|
||||
...mockPeriod,
|
||||
status: PeriodStatus.CLOSED,
|
||||
closedAt: new Date(),
|
||||
closedBy: mockUserId,
|
||||
};
|
||||
periodsService.close.mockResolvedValue(closedPeriod);
|
||||
|
||||
const result = await controller.close(mockRequestUser, mockPeriodId, closeDto);
|
||||
|
||||
expect(periodsService.close).toHaveBeenCalledWith(mockTenantId, mockPeriodId, mockUserId, closeDto);
|
||||
expect(result.status).toBe(PeriodStatus.CLOSED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('markPaid', () => {
|
||||
it('should mark a period as paid', async () => {
|
||||
const markPaidDto: MarkPaidDto = {
|
||||
paymentReference: 'PAY-2026-001',
|
||||
paymentNotes: 'Bank transfer completed',
|
||||
};
|
||||
const paidPeriod = {
|
||||
...mockPeriod,
|
||||
status: PeriodStatus.PAID,
|
||||
paidAt: new Date(),
|
||||
paidBy: mockUserId,
|
||||
paymentReference: markPaidDto.paymentReference,
|
||||
};
|
||||
periodsService.markAsPaid.mockResolvedValue(paidPeriod);
|
||||
|
||||
const result = await controller.markPaid(mockRequestUser, mockPeriodId, markPaidDto);
|
||||
|
||||
expect(periodsService.markAsPaid).toHaveBeenCalledWith(mockTenantId, mockPeriodId, mockUserId, markPaidDto);
|
||||
expect(result.status).toBe(PeriodStatus.PAID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should delete a period', async () => {
|
||||
periodsService.remove.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.remove(mockRequestUser, mockPeriodId);
|
||||
|
||||
expect(periodsService.remove).toHaveBeenCalledWith(mockTenantId, mockPeriodId);
|
||||
expect(result).toEqual({ message: 'Commission period deleted successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignEntries', () => {
|
||||
it('should assign entries to a period', async () => {
|
||||
const entryIds = ['entry-1', 'entry-2', 'entry-3'];
|
||||
periodsService.assignEntriesToPeriod.mockResolvedValue(3);
|
||||
|
||||
const result = await controller.assignEntries(mockRequestUser, mockPeriodId, { entryIds });
|
||||
|
||||
expect(periodsService.assignEntriesToPeriod).toHaveBeenCalledWith(mockTenantId, mockPeriodId, entryIds);
|
||||
expect(result).toEqual({ assigned: 3 });
|
||||
});
|
||||
|
||||
it('should return count of assigned entries', async () => {
|
||||
const entryIds = ['entry-1', 'entry-2'];
|
||||
periodsService.assignEntriesToPeriod.mockResolvedValue(1);
|
||||
|
||||
const result = await controller.assignEntries(mockRequestUser, mockPeriodId, { entryIds });
|
||||
|
||||
expect(result.assigned).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
196
src/modules/commissions/__tests__/schemes.controller.spec.ts
Normal file
196
src/modules/commissions/__tests__/schemes.controller.spec.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SchemesController } from '../controllers/schemes.controller';
|
||||
import { SchemesService } from '../services/schemes.service';
|
||||
import { CreateSchemeDto, UpdateSchemeDto, SchemeListQueryDto } from '../dto';
|
||||
import { SchemeType, AppliesTo } from '../entities/commission-scheme.entity';
|
||||
|
||||
describe('SchemesController', () => {
|
||||
let controller: SchemesController;
|
||||
let schemesService: jest.Mocked<SchemesService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockSchemeId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockScheme = {
|
||||
id: mockSchemeId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'Sales Commission',
|
||||
description: '10% on all sales',
|
||||
type: SchemeType.PERCENTAGE,
|
||||
rate: 10,
|
||||
fixedAmount: 0,
|
||||
tiers: [],
|
||||
appliesTo: AppliesTo.ALL,
|
||||
productIds: [],
|
||||
categoryIds: [],
|
||||
minAmount: 0,
|
||||
maxAmount: null,
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
createdBy: mockUserId,
|
||||
};
|
||||
|
||||
const mockPaginatedSchemes = {
|
||||
data: [mockScheme],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockSchemesService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
activate: jest.fn(),
|
||||
deactivate: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SchemesController],
|
||||
providers: [
|
||||
{
|
||||
provide: SchemesService,
|
||||
useValue: mockSchemesService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SchemesController>(SchemesController);
|
||||
schemesService = module.get(SchemesService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated schemes', async () => {
|
||||
const query: SchemeListQueryDto = { page: 1, limit: 20 };
|
||||
schemesService.findAll.mockResolvedValue(mockPaginatedSchemes);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(schemesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
expect(result).toEqual(mockPaginatedSchemes);
|
||||
});
|
||||
|
||||
it('should filter by type', async () => {
|
||||
const query: SchemeListQueryDto = { page: 1, limit: 20, type: SchemeType.TIERED };
|
||||
schemesService.findAll.mockResolvedValue({ ...mockPaginatedSchemes, data: [] });
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(schemesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a scheme by id', async () => {
|
||||
schemesService.findOne.mockResolvedValue(mockScheme);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockSchemeId);
|
||||
|
||||
expect(schemesService.findOne).toHaveBeenCalledWith(mockTenantId, mockSchemeId);
|
||||
expect(result).toEqual(mockScheme);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new scheme', async () => {
|
||||
const createDto: CreateSchemeDto = {
|
||||
name: 'Premium Commission',
|
||||
type: SchemeType.PERCENTAGE,
|
||||
rate: 15,
|
||||
appliesTo: AppliesTo.PRODUCTS,
|
||||
};
|
||||
const createdScheme = { ...mockScheme, ...createDto, id: 'new-scheme-id' };
|
||||
schemesService.create.mockResolvedValue(createdScheme);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(schemesService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result).toEqual(createdScheme);
|
||||
});
|
||||
|
||||
it('should create a tiered scheme', async () => {
|
||||
const createDto: CreateSchemeDto = {
|
||||
name: 'Tiered Commission',
|
||||
type: SchemeType.TIERED,
|
||||
tiers: [
|
||||
{ minAmount: 0, maxAmount: 1000, rate: 5 },
|
||||
{ minAmount: 1001, maxAmount: 5000, rate: 10 },
|
||||
{ minAmount: 5001, rate: 15 },
|
||||
],
|
||||
};
|
||||
schemesService.create.mockResolvedValue({ ...mockScheme, ...createDto });
|
||||
|
||||
await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(schemesService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a scheme', async () => {
|
||||
const updateDto: UpdateSchemeDto = {
|
||||
rate: 12,
|
||||
description: 'Updated commission description',
|
||||
};
|
||||
const updatedScheme = { ...mockScheme, ...updateDto };
|
||||
schemesService.update.mockResolvedValue(updatedScheme);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockSchemeId, updateDto);
|
||||
|
||||
expect(schemesService.update).toHaveBeenCalledWith(mockTenantId, mockSchemeId, updateDto);
|
||||
expect(result.rate).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should delete a scheme', async () => {
|
||||
schemesService.remove.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.remove(mockRequestUser, mockSchemeId);
|
||||
|
||||
expect(schemesService.remove).toHaveBeenCalledWith(mockTenantId, mockSchemeId);
|
||||
expect(result).toEqual({ message: 'Commission scheme deleted successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('activate', () => {
|
||||
it('should activate a scheme', async () => {
|
||||
const activatedScheme = { ...mockScheme, isActive: true };
|
||||
schemesService.activate.mockResolvedValue(activatedScheme);
|
||||
|
||||
const result = await controller.activate(mockRequestUser, mockSchemeId);
|
||||
|
||||
expect(schemesService.activate).toHaveBeenCalledWith(mockTenantId, mockSchemeId);
|
||||
expect(result.isActive).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deactivate', () => {
|
||||
it('should deactivate a scheme', async () => {
|
||||
const deactivatedScheme = { ...mockScheme, isActive: false };
|
||||
schemesService.deactivate.mockResolvedValue(deactivatedScheme);
|
||||
|
||||
const result = await controller.deactivate(mockRequestUser, mockSchemeId);
|
||||
|
||||
expect(schemesService.deactivate).toHaveBeenCalledWith(mockTenantId, mockSchemeId);
|
||||
expect(result.isActive).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
235
src/modules/portfolio/__tests__/categories.controller.spec.ts
Normal file
235
src/modules/portfolio/__tests__/categories.controller.spec.ts
Normal file
@ -0,0 +1,235 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CategoriesController } from '../controllers/categories.controller';
|
||||
import { CategoriesService } from '../services/categories.service';
|
||||
import { CreateCategoryDto, UpdateCategoryDto, CategoryListQueryDto } from '../dto';
|
||||
|
||||
describe('CategoriesController', () => {
|
||||
let controller: CategoriesController;
|
||||
let categoriesService: jest.Mocked<CategoriesService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockCategoryId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockCategory = {
|
||||
id: mockCategoryId,
|
||||
tenantId: mockTenantId,
|
||||
parentId: null,
|
||||
name: 'Electronics',
|
||||
slug: 'electronics',
|
||||
description: 'Electronic products',
|
||||
position: 1,
|
||||
imageUrl: null,
|
||||
color: '#3B82F6',
|
||||
icon: 'laptop',
|
||||
isActive: true,
|
||||
metaTitle: null,
|
||||
metaDescription: null,
|
||||
customFields: {},
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
createdBy: mockUserId,
|
||||
};
|
||||
|
||||
const mockPaginatedCategories = {
|
||||
data: [mockCategory],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
const mockCategoryTree = [
|
||||
{
|
||||
...mockCategory,
|
||||
children: [
|
||||
{
|
||||
id: 'child-1',
|
||||
name: 'Phones',
|
||||
slug: 'phones',
|
||||
parentId: mockCategoryId,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'child-2',
|
||||
name: 'Laptops',
|
||||
slug: 'laptops',
|
||||
parentId: mockCategoryId,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockCategoriesService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
getTree: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [CategoriesController],
|
||||
providers: [
|
||||
{
|
||||
provide: CategoriesService,
|
||||
useValue: mockCategoriesService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<CategoriesController>(CategoriesController);
|
||||
categoriesService = module.get(CategoriesService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated categories', async () => {
|
||||
const query: CategoryListQueryDto = { page: 1, limit: 20 };
|
||||
categoriesService.findAll.mockResolvedValue(mockPaginatedCategories);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(categoriesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
expect(result).toEqual(mockPaginatedCategories);
|
||||
});
|
||||
|
||||
it('should filter by active status', async () => {
|
||||
const query: CategoryListQueryDto = { page: 1, limit: 20, isActive: true };
|
||||
categoriesService.findAll.mockResolvedValue(mockPaginatedCategories);
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(categoriesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
|
||||
it('should search by name', async () => {
|
||||
const query: CategoryListQueryDto = { page: 1, limit: 20, search: 'electronics' };
|
||||
categoriesService.findAll.mockResolvedValue(mockPaginatedCategories);
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(categoriesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTree', () => {
|
||||
it('should return category tree structure', async () => {
|
||||
categoriesService.getTree.mockResolvedValue(mockCategoryTree);
|
||||
|
||||
const result = await controller.getTree(mockRequestUser);
|
||||
|
||||
expect(categoriesService.getTree).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockCategoryTree);
|
||||
expect(result[0].children.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a category by id', async () => {
|
||||
categoriesService.findOne.mockResolvedValue(mockCategory);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockCategoryId);
|
||||
|
||||
expect(categoriesService.findOne).toHaveBeenCalledWith(mockTenantId, mockCategoryId);
|
||||
expect(result).toEqual(mockCategory);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new category', async () => {
|
||||
const createDto: CreateCategoryDto = {
|
||||
name: 'Clothing',
|
||||
slug: 'clothing',
|
||||
description: 'Fashion and clothing',
|
||||
};
|
||||
const createdCategory = { ...mockCategory, ...createDto, id: 'new-category-id' };
|
||||
categoriesService.create.mockResolvedValue(createdCategory);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(categoriesService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result).toEqual(createdCategory);
|
||||
});
|
||||
|
||||
it('should create a subcategory', async () => {
|
||||
const createDto: CreateCategoryDto = {
|
||||
name: 'Smartphones',
|
||||
slug: 'smartphones',
|
||||
parentId: mockCategoryId,
|
||||
};
|
||||
const createdSubcategory = { ...mockCategory, ...createDto, id: 'sub-category-id' };
|
||||
categoriesService.create.mockResolvedValue(createdSubcategory);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(result.parentId).toBe(mockCategoryId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a category', async () => {
|
||||
const updateDto: UpdateCategoryDto = {
|
||||
name: 'Updated Electronics',
|
||||
color: '#EF4444',
|
||||
};
|
||||
const updatedCategory = { ...mockCategory, ...updateDto };
|
||||
categoriesService.update.mockResolvedValue(updatedCategory);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockCategoryId, updateDto);
|
||||
|
||||
expect(categoriesService.update).toHaveBeenCalledWith(mockTenantId, mockCategoryId, updateDto);
|
||||
expect(result.name).toBe('Updated Electronics');
|
||||
expect(result.color).toBe('#EF4444');
|
||||
});
|
||||
|
||||
it('should deactivate a category', async () => {
|
||||
const updateDto: UpdateCategoryDto = {
|
||||
isActive: false,
|
||||
};
|
||||
const deactivatedCategory = { ...mockCategory, isActive: false };
|
||||
categoriesService.update.mockResolvedValue(deactivatedCategory);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockCategoryId, updateDto);
|
||||
|
||||
expect(result.isActive).toBe(false);
|
||||
});
|
||||
|
||||
it('should move category to different parent', async () => {
|
||||
const newParentId = 'new-parent-id';
|
||||
const updateDto: UpdateCategoryDto = {
|
||||
parentId: newParentId,
|
||||
};
|
||||
const movedCategory = { ...mockCategory, parentId: newParentId };
|
||||
categoriesService.update.mockResolvedValue(movedCategory);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockCategoryId, updateDto);
|
||||
|
||||
expect(result.parentId).toBe(newParentId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should delete a category', async () => {
|
||||
categoriesService.remove.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.remove(mockRequestUser, mockCategoryId);
|
||||
|
||||
expect(categoriesService.remove).toHaveBeenCalledWith(mockTenantId, mockCategoryId);
|
||||
expect(result).toEqual({ message: 'Category deleted successfully' });
|
||||
});
|
||||
});
|
||||
});
|
||||
367
src/modules/portfolio/__tests__/products.controller.spec.ts
Normal file
367
src/modules/portfolio/__tests__/products.controller.spec.ts
Normal file
@ -0,0 +1,367 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ProductsController } from '../controllers/products.controller';
|
||||
import { ProductsService } from '../services/products.service';
|
||||
import {
|
||||
CreateProductDto,
|
||||
UpdateProductDto,
|
||||
UpdateProductStatusDto,
|
||||
ProductListQueryDto,
|
||||
CreateVariantDto,
|
||||
UpdateVariantDto,
|
||||
CreatePriceDto,
|
||||
UpdatePriceDto,
|
||||
} from '../dto';
|
||||
import { ProductType, ProductStatus } from '../entities/product.entity';
|
||||
|
||||
describe('ProductsController', () => {
|
||||
let controller: ProductsController;
|
||||
let productsService: jest.Mocked<ProductsService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockProductId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
const mockVariantId = '550e8400-e29b-41d4-a716-446655440004';
|
||||
const mockPriceId = '550e8400-e29b-41d4-a716-446655440005';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockProduct = {
|
||||
id: mockProductId,
|
||||
tenantId: mockTenantId,
|
||||
categoryId: 'cat-123',
|
||||
name: 'Wireless Headphones',
|
||||
slug: 'wireless-headphones',
|
||||
sku: 'WH-001',
|
||||
barcode: '1234567890123',
|
||||
description: 'Premium wireless headphones',
|
||||
shortDescription: 'High quality wireless headphones',
|
||||
productType: ProductType.PHYSICAL,
|
||||
status: ProductStatus.ACTIVE,
|
||||
basePrice: 149.99,
|
||||
costPrice: 80,
|
||||
compareAtPrice: 199.99,
|
||||
currency: 'USD',
|
||||
trackInventory: true,
|
||||
stockQuantity: 100,
|
||||
lowStockThreshold: 10,
|
||||
allowBackorder: false,
|
||||
weight: 0.5,
|
||||
images: [],
|
||||
tags: ['audio', 'wireless'],
|
||||
isVisible: true,
|
||||
isFeatured: false,
|
||||
hasVariants: true,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
createdBy: mockUserId,
|
||||
};
|
||||
|
||||
const mockVariant = {
|
||||
id: mockVariantId,
|
||||
productId: mockProductId,
|
||||
name: 'Black',
|
||||
sku: 'WH-001-BLK',
|
||||
price: 149.99,
|
||||
stockQuantity: 50,
|
||||
attributes: { color: 'Black' },
|
||||
isDefault: true,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
};
|
||||
|
||||
const mockPrice = {
|
||||
id: mockPriceId,
|
||||
productId: mockProductId,
|
||||
name: 'Retail',
|
||||
amount: 149.99,
|
||||
currency: 'USD',
|
||||
isDefault: true,
|
||||
minQuantity: 1,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
};
|
||||
|
||||
const mockPaginatedProducts = {
|
||||
data: [mockProduct],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockProductsService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
duplicate: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
getVariants: jest.fn(),
|
||||
createVariant: jest.fn(),
|
||||
updateVariant: jest.fn(),
|
||||
removeVariant: jest.fn(),
|
||||
getPrices: jest.fn(),
|
||||
createPrice: jest.fn(),
|
||||
updatePrice: jest.fn(),
|
||||
removePrice: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [ProductsController],
|
||||
providers: [
|
||||
{
|
||||
provide: ProductsService,
|
||||
useValue: mockProductsService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<ProductsController>(ProductsController);
|
||||
productsService = module.get(ProductsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Products Tests
|
||||
// ============================================
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated products', async () => {
|
||||
const query: ProductListQueryDto = { page: 1, limit: 20 };
|
||||
productsService.findAll.mockResolvedValue(mockPaginatedProducts);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(productsService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
expect(result).toEqual(mockPaginatedProducts);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const query: ProductListQueryDto = { page: 1, limit: 20, status: ProductStatus.ACTIVE };
|
||||
productsService.findAll.mockResolvedValue(mockPaginatedProducts);
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(productsService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
|
||||
it('should filter by category', async () => {
|
||||
const query: ProductListQueryDto = { page: 1, limit: 20, categoryId: 'cat-123' };
|
||||
productsService.findAll.mockResolvedValue(mockPaginatedProducts);
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(productsService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a product by id', async () => {
|
||||
productsService.findOne.mockResolvedValue(mockProduct);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockProductId);
|
||||
|
||||
expect(productsService.findOne).toHaveBeenCalledWith(mockTenantId, mockProductId);
|
||||
expect(result).toEqual(mockProduct);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new product', async () => {
|
||||
const createDto: CreateProductDto = {
|
||||
name: 'New Headphones',
|
||||
slug: 'new-headphones',
|
||||
basePrice: 99.99,
|
||||
productType: ProductType.PHYSICAL,
|
||||
};
|
||||
const createdProduct = { ...mockProduct, ...createDto, id: 'new-product-id' };
|
||||
productsService.create.mockResolvedValue(createdProduct);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(productsService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result).toEqual(createdProduct);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a product', async () => {
|
||||
const updateDto: UpdateProductDto = {
|
||||
name: 'Updated Headphones',
|
||||
basePrice: 129.99,
|
||||
};
|
||||
const updatedProduct = { ...mockProduct, ...updateDto };
|
||||
productsService.update.mockResolvedValue(updatedProduct);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockProductId, updateDto);
|
||||
|
||||
expect(productsService.update).toHaveBeenCalledWith(mockTenantId, mockProductId, updateDto);
|
||||
expect(result.name).toBe('Updated Headphones');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('should update product status', async () => {
|
||||
const statusDto: UpdateProductStatusDto = {
|
||||
status: ProductStatus.INACTIVE,
|
||||
};
|
||||
const updatedProduct = { ...mockProduct, status: ProductStatus.INACTIVE };
|
||||
productsService.updateStatus.mockResolvedValue(updatedProduct);
|
||||
|
||||
const result = await controller.updateStatus(mockRequestUser, mockProductId, statusDto);
|
||||
|
||||
expect(productsService.updateStatus).toHaveBeenCalledWith(mockTenantId, mockProductId, statusDto);
|
||||
expect(result.status).toBe(ProductStatus.INACTIVE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicate', () => {
|
||||
it('should duplicate a product', async () => {
|
||||
const duplicatedProduct = { ...mockProduct, id: 'duplicated-id', name: 'Wireless Headphones (Copy)' };
|
||||
productsService.duplicate.mockResolvedValue(duplicatedProduct);
|
||||
|
||||
const result = await controller.duplicate(mockRequestUser, mockProductId);
|
||||
|
||||
expect(productsService.duplicate).toHaveBeenCalledWith(mockTenantId, mockUserId, mockProductId);
|
||||
expect(result.id).toBe('duplicated-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should delete a product', async () => {
|
||||
productsService.remove.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.remove(mockRequestUser, mockProductId);
|
||||
|
||||
expect(productsService.remove).toHaveBeenCalledWith(mockTenantId, mockProductId);
|
||||
expect(result).toEqual({ message: 'Product deleted successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Variants Tests
|
||||
// ============================================
|
||||
|
||||
describe('getVariants', () => {
|
||||
it('should return product variants', async () => {
|
||||
productsService.getVariants.mockResolvedValue([mockVariant]);
|
||||
|
||||
const result = await controller.getVariants(mockRequestUser, mockProductId);
|
||||
|
||||
expect(productsService.getVariants).toHaveBeenCalledWith(mockTenantId, mockProductId);
|
||||
expect(result).toEqual([mockVariant]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createVariant', () => {
|
||||
it('should create a product variant', async () => {
|
||||
const createDto: CreateVariantDto = {
|
||||
name: 'White',
|
||||
sku: 'WH-001-WHT',
|
||||
price: 149.99,
|
||||
attributes: { color: 'White' },
|
||||
};
|
||||
const createdVariant = { ...mockVariant, ...createDto, id: 'new-variant-id' };
|
||||
productsService.createVariant.mockResolvedValue(createdVariant);
|
||||
|
||||
const result = await controller.createVariant(mockRequestUser, mockProductId, createDto);
|
||||
|
||||
expect(productsService.createVariant).toHaveBeenCalledWith(mockTenantId, mockProductId, createDto);
|
||||
expect(result).toEqual(createdVariant);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateVariant', () => {
|
||||
it('should update a product variant', async () => {
|
||||
const updateDto: UpdateVariantDto = {
|
||||
price: 139.99,
|
||||
stockQuantity: 75,
|
||||
};
|
||||
const updatedVariant = { ...mockVariant, ...updateDto };
|
||||
productsService.updateVariant.mockResolvedValue(updatedVariant);
|
||||
|
||||
const result = await controller.updateVariant(mockRequestUser, mockProductId, mockVariantId, updateDto);
|
||||
|
||||
expect(productsService.updateVariant).toHaveBeenCalledWith(mockTenantId, mockProductId, mockVariantId, updateDto);
|
||||
expect(result.price).toBe(139.99);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeVariant', () => {
|
||||
it('should delete a product variant', async () => {
|
||||
productsService.removeVariant.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.removeVariant(mockRequestUser, mockProductId, mockVariantId);
|
||||
|
||||
expect(productsService.removeVariant).toHaveBeenCalledWith(mockTenantId, mockProductId, mockVariantId);
|
||||
expect(result).toEqual({ message: 'Variant deleted successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Prices Tests
|
||||
// ============================================
|
||||
|
||||
describe('getPrices', () => {
|
||||
it('should return product prices', async () => {
|
||||
productsService.getPrices.mockResolvedValue([mockPrice]);
|
||||
|
||||
const result = await controller.getPrices(mockRequestUser, mockProductId);
|
||||
|
||||
expect(productsService.getPrices).toHaveBeenCalledWith(mockTenantId, mockProductId);
|
||||
expect(result).toEqual([mockPrice]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPrice', () => {
|
||||
it('should create a product price', async () => {
|
||||
const createDto: CreatePriceDto = {
|
||||
name: 'Wholesale',
|
||||
amount: 99.99,
|
||||
currency: 'USD',
|
||||
minQuantity: 10,
|
||||
};
|
||||
const createdPrice = { ...mockPrice, ...createDto, id: 'new-price-id' };
|
||||
productsService.createPrice.mockResolvedValue(createdPrice);
|
||||
|
||||
const result = await controller.createPrice(mockRequestUser, mockProductId, createDto);
|
||||
|
||||
expect(productsService.createPrice).toHaveBeenCalledWith(mockTenantId, mockProductId, createDto);
|
||||
expect(result).toEqual(createdPrice);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePrice', () => {
|
||||
it('should update a product price', async () => {
|
||||
const updateDto: UpdatePriceDto = {
|
||||
amount: 159.99,
|
||||
};
|
||||
const updatedPrice = { ...mockPrice, ...updateDto };
|
||||
productsService.updatePrice.mockResolvedValue(updatedPrice);
|
||||
|
||||
const result = await controller.updatePrice(mockRequestUser, mockProductId, mockPriceId, updateDto);
|
||||
|
||||
expect(productsService.updatePrice).toHaveBeenCalledWith(mockTenantId, mockProductId, mockPriceId, updateDto);
|
||||
expect(result.amount).toBe(159.99);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removePrice', () => {
|
||||
it('should delete a product price', async () => {
|
||||
productsService.removePrice.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.removePrice(mockRequestUser, mockProductId, mockPriceId);
|
||||
|
||||
expect(productsService.removePrice).toHaveBeenCalledWith(mockTenantId, mockProductId, mockPriceId);
|
||||
expect(result).toEqual({ message: 'Price deleted successfully' });
|
||||
});
|
||||
});
|
||||
});
|
||||
179
src/modules/sales/__tests__/activities.controller.spec.ts
Normal file
179
src/modules/sales/__tests__/activities.controller.spec.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ActivitiesController } from '../controllers/activities.controller';
|
||||
import { ActivitiesService } from '../services/activities.service';
|
||||
import { CreateActivityDto, UpdateActivityDto, CompleteActivityDto, ActivityListQueryDto } from '../dto';
|
||||
import { ActivityType, ActivityStatus } from '../entities/activity.entity';
|
||||
|
||||
describe('ActivitiesController', () => {
|
||||
let controller: ActivitiesController;
|
||||
let activitiesService: jest.Mocked<ActivitiesService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockActivityId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockActivity = {
|
||||
id: mockActivityId,
|
||||
tenantId: mockTenantId,
|
||||
type: ActivityType.CALL,
|
||||
status: ActivityStatus.PENDING,
|
||||
subject: 'Follow up call',
|
||||
description: 'Discuss proposal',
|
||||
dueDate: new Date('2026-02-10'),
|
||||
assignedTo: mockUserId,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
createdBy: mockUserId,
|
||||
};
|
||||
|
||||
const mockPaginatedActivities = {
|
||||
data: [mockActivity],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockActivitiesService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
complete: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
getUpcoming: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [ActivitiesController],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivitiesService,
|
||||
useValue: mockActivitiesService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<ActivitiesController>(ActivitiesController);
|
||||
activitiesService = module.get(ActivitiesService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated activities', async () => {
|
||||
const query: ActivityListQueryDto = { page: 1, limit: 20 };
|
||||
activitiesService.findAll.mockResolvedValue(mockPaginatedActivities);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(activitiesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
expect(result).toEqual(mockPaginatedActivities);
|
||||
});
|
||||
|
||||
it('should filter by type', async () => {
|
||||
const query: ActivityListQueryDto = { page: 1, limit: 20, type: ActivityType.MEETING };
|
||||
activitiesService.findAll.mockResolvedValue({ ...mockPaginatedActivities, data: [] });
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(activitiesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpcoming', () => {
|
||||
it('should return upcoming activities', async () => {
|
||||
activitiesService.getUpcoming.mockResolvedValue([mockActivity]);
|
||||
|
||||
const result = await controller.getUpcoming(mockRequestUser);
|
||||
|
||||
expect(activitiesService.getUpcoming).toHaveBeenCalledWith(mockTenantId, mockUserId);
|
||||
expect(result).toEqual([mockActivity]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return an activity by id', async () => {
|
||||
activitiesService.findOne.mockResolvedValue(mockActivity);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockActivityId);
|
||||
|
||||
expect(activitiesService.findOne).toHaveBeenCalledWith(mockTenantId, mockActivityId);
|
||||
expect(result).toEqual(mockActivity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new activity', async () => {
|
||||
const createDto: CreateActivityDto = {
|
||||
type: ActivityType.CALL,
|
||||
subject: 'Initial call',
|
||||
dueDate: '2026-02-15',
|
||||
};
|
||||
const createdActivity = { ...mockActivity, ...createDto, id: 'new-activity-id' };
|
||||
activitiesService.create.mockResolvedValue(createdActivity);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(activitiesService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result).toEqual(createdActivity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update an activity', async () => {
|
||||
const updateDto: UpdateActivityDto = {
|
||||
subject: 'Updated call subject',
|
||||
status: ActivityStatus.COMPLETED,
|
||||
};
|
||||
const updatedActivity = { ...mockActivity, ...updateDto };
|
||||
activitiesService.update.mockResolvedValue(updatedActivity);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockActivityId, updateDto);
|
||||
|
||||
expect(activitiesService.update).toHaveBeenCalledWith(mockTenantId, mockActivityId, updateDto);
|
||||
expect(result.subject).toBe('Updated call subject');
|
||||
});
|
||||
});
|
||||
|
||||
describe('complete', () => {
|
||||
it('should complete an activity', async () => {
|
||||
const completeDto: CompleteActivityDto = {
|
||||
outcome: 'Discussed proposal details',
|
||||
};
|
||||
const completedActivity = {
|
||||
...mockActivity,
|
||||
status: ActivityStatus.COMPLETED,
|
||||
completedAt: new Date(),
|
||||
outcome: completeDto.outcome,
|
||||
};
|
||||
activitiesService.complete.mockResolvedValue(completedActivity);
|
||||
|
||||
const result = await controller.complete(mockRequestUser, mockActivityId, completeDto);
|
||||
|
||||
expect(activitiesService.complete).toHaveBeenCalledWith(mockTenantId, mockActivityId, completeDto);
|
||||
expect(result.status).toBe(ActivityStatus.COMPLETED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should delete an activity', async () => {
|
||||
activitiesService.remove.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.remove(mockRequestUser, mockActivityId);
|
||||
|
||||
expect(activitiesService.remove).toHaveBeenCalledWith(mockTenantId, mockActivityId);
|
||||
expect(result).toEqual({ message: 'Activity deleted successfully' });
|
||||
});
|
||||
});
|
||||
});
|
||||
171
src/modules/sales/__tests__/dashboard.controller.spec.ts
Normal file
171
src/modules/sales/__tests__/dashboard.controller.spec.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SalesDashboardController } from '../controllers/dashboard.controller';
|
||||
import { SalesDashboardService } from '../services/sales-dashboard.service';
|
||||
import { LeadStatus, LeadSource } from '../entities/lead.entity';
|
||||
import { OpportunityStage } from '../entities/opportunity.entity';
|
||||
|
||||
describe('SalesDashboardController', () => {
|
||||
let controller: SalesDashboardController;
|
||||
let dashboardService: jest.Mocked<SalesDashboardService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockDashboardSummary = {
|
||||
totalLeads: 150,
|
||||
newLeadsThisMonth: 25,
|
||||
qualifiedLeads: 45,
|
||||
convertedLeads: 30,
|
||||
totalOpportunities: 80,
|
||||
openOpportunities: 50,
|
||||
wonOpportunities: 25,
|
||||
lostOpportunities: 5,
|
||||
totalPipelineValue: 500000,
|
||||
wonValue: 150000,
|
||||
averageDealSize: 6000,
|
||||
winRate: 83.33,
|
||||
};
|
||||
|
||||
const mockLeadsByStatus = [
|
||||
{ status: LeadStatus.NEW, count: 50 },
|
||||
{ status: LeadStatus.CONTACTED, count: 35 },
|
||||
{ status: LeadStatus.QUALIFIED, count: 45 },
|
||||
{ status: LeadStatus.CONVERTED, count: 20 },
|
||||
];
|
||||
|
||||
const mockLeadsBySource = [
|
||||
{ source: LeadSource.WEBSITE, count: 60 },
|
||||
{ source: LeadSource.REFERRAL, count: 40 },
|
||||
{ source: LeadSource.SOCIAL_MEDIA, count: 25 },
|
||||
{ source: LeadSource.EVENT, count: 25 },
|
||||
];
|
||||
|
||||
const mockOpportunitiesByStage = [
|
||||
{ stage: OpportunityStage.PROSPECTING, count: 15, value: 75000 },
|
||||
{ stage: OpportunityStage.QUALIFICATION, count: 12, value: 60000 },
|
||||
{ stage: OpportunityStage.PROPOSAL, count: 10, value: 100000 },
|
||||
{ stage: OpportunityStage.NEGOTIATION, count: 8, value: 120000 },
|
||||
{ stage: OpportunityStage.CLOSED_WON, count: 25, value: 150000 },
|
||||
];
|
||||
|
||||
const mockConversionFunnel = [
|
||||
{ stage: 'Lead Created', count: 150, percentage: 100 },
|
||||
{ stage: 'Qualified', count: 45, percentage: 30 },
|
||||
{ stage: 'Opportunity Created', count: 35, percentage: 23 },
|
||||
{ stage: 'Proposal Sent', count: 25, percentage: 17 },
|
||||
{ stage: 'Won', count: 20, percentage: 13 },
|
||||
];
|
||||
|
||||
const mockSalesPerformance = [
|
||||
{ userId: mockUserId, name: 'John Doe', leadsConverted: 10, opportunitiesWon: 5, totalValue: 50000 },
|
||||
{ userId: 'user-2', name: 'Jane Smith', leadsConverted: 8, opportunitiesWon: 4, totalValue: 40000 },
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockDashboardService = {
|
||||
getDashboardSummary: jest.fn(),
|
||||
getLeadsByStatus: jest.fn(),
|
||||
getLeadsBySource: jest.fn(),
|
||||
getOpportunitiesByStage: jest.fn(),
|
||||
getConversionFunnel: jest.fn(),
|
||||
getSalesPerformance: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SalesDashboardController],
|
||||
providers: [
|
||||
{
|
||||
provide: SalesDashboardService,
|
||||
useValue: mockDashboardService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SalesDashboardController>(SalesDashboardController);
|
||||
dashboardService = module.get(SalesDashboardService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getSummary', () => {
|
||||
it('should return dashboard summary', async () => {
|
||||
dashboardService.getDashboardSummary.mockResolvedValue(mockDashboardSummary);
|
||||
|
||||
const result = await controller.getSummary(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getDashboardSummary).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockDashboardSummary);
|
||||
expect(result.totalLeads).toBe(150);
|
||||
expect(result.winRate).toBe(83.33);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeadsByStatus', () => {
|
||||
it('should return leads grouped by status', async () => {
|
||||
dashboardService.getLeadsByStatus.mockResolvedValue(mockLeadsByStatus);
|
||||
|
||||
const result = await controller.getLeadsByStatus(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getLeadsByStatus).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockLeadsByStatus);
|
||||
expect(result.length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeadsBySource', () => {
|
||||
it('should return leads grouped by source', async () => {
|
||||
dashboardService.getLeadsBySource.mockResolvedValue(mockLeadsBySource);
|
||||
|
||||
const result = await controller.getLeadsBySource(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getLeadsBySource).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockLeadsBySource);
|
||||
expect(result.length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOpportunitiesByStage', () => {
|
||||
it('should return opportunities grouped by stage', async () => {
|
||||
dashboardService.getOpportunitiesByStage.mockResolvedValue(mockOpportunitiesByStage);
|
||||
|
||||
const result = await controller.getOpportunitiesByStage(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getOpportunitiesByStage).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockOpportunitiesByStage);
|
||||
expect(result.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConversionFunnel', () => {
|
||||
it('should return conversion funnel data', async () => {
|
||||
dashboardService.getConversionFunnel.mockResolvedValue(mockConversionFunnel);
|
||||
|
||||
const result = await controller.getConversionFunnel(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getConversionFunnel).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockConversionFunnel);
|
||||
expect(result[0].percentage).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSalesPerformance', () => {
|
||||
it('should return sales performance by user', async () => {
|
||||
dashboardService.getSalesPerformance.mockResolvedValue(mockSalesPerformance);
|
||||
|
||||
const result = await controller.getSalesPerformance(mockRequestUser);
|
||||
|
||||
expect(dashboardService.getSalesPerformance).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockSalesPerformance);
|
||||
expect(result.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
219
src/modules/sales/__tests__/leads.controller.spec.ts
Normal file
219
src/modules/sales/__tests__/leads.controller.spec.ts
Normal file
@ -0,0 +1,219 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LeadsController } from '../controllers/leads.controller';
|
||||
import { LeadsService } from '../services/leads.service';
|
||||
import { CreateLeadDto, UpdateLeadDto, ConvertLeadDto, LeadListQueryDto } from '../dto';
|
||||
import { LeadStatus, LeadSource } from '../entities/lead.entity';
|
||||
|
||||
describe('LeadsController', () => {
|
||||
let controller: LeadsController;
|
||||
let leadsService: jest.Mocked<LeadsService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockLeadId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockLead = {
|
||||
id: mockLeadId,
|
||||
tenantId: mockTenantId,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@example.com',
|
||||
phone: '+1234567890',
|
||||
company: 'Acme Corp',
|
||||
jobTitle: 'CEO',
|
||||
source: LeadSource.WEBSITE,
|
||||
status: LeadStatus.NEW,
|
||||
score: 50,
|
||||
assignedTo: mockUserId,
|
||||
notes: 'Interested in enterprise plan',
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
createdBy: mockUserId,
|
||||
};
|
||||
|
||||
const mockPaginatedLeads = {
|
||||
data: [mockLead],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockLeadsService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
convert: jest.fn(),
|
||||
calculateScore: jest.fn(),
|
||||
assign: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [LeadsController],
|
||||
providers: [
|
||||
{
|
||||
provide: LeadsService,
|
||||
useValue: mockLeadsService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<LeadsController>(LeadsController);
|
||||
leadsService = module.get(LeadsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated leads', async () => {
|
||||
const query: LeadListQueryDto = { page: 1, limit: 20 };
|
||||
leadsService.findAll.mockResolvedValue(mockPaginatedLeads);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(leadsService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
expect(result).toEqual(mockPaginatedLeads);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const query: LeadListQueryDto = { page: 1, limit: 20, status: LeadStatus.QUALIFIED };
|
||||
leadsService.findAll.mockResolvedValue({ ...mockPaginatedLeads, data: [] });
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(leadsService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
|
||||
it('should filter by search term', async () => {
|
||||
const query: LeadListQueryDto = { page: 1, limit: 20, search: 'john' };
|
||||
leadsService.findAll.mockResolvedValue(mockPaginatedLeads);
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(leadsService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a lead by id', async () => {
|
||||
leadsService.findOne.mockResolvedValue(mockLead);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockLeadId);
|
||||
|
||||
expect(leadsService.findOne).toHaveBeenCalledWith(mockTenantId, mockLeadId);
|
||||
expect(result).toEqual(mockLead);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new lead', async () => {
|
||||
const createDto: CreateLeadDto = {
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
email: 'jane.smith@example.com',
|
||||
source: LeadSource.REFERRAL,
|
||||
};
|
||||
const createdLead = { ...mockLead, ...createDto, id: 'new-lead-id' };
|
||||
leadsService.create.mockResolvedValue(createdLead);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(leadsService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result).toEqual(createdLead);
|
||||
});
|
||||
|
||||
it('should create lead with all fields', async () => {
|
||||
const createDto: CreateLeadDto = {
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
email: 'jane.smith@example.com',
|
||||
phone: '+1987654321',
|
||||
company: 'Tech Corp',
|
||||
jobTitle: 'CTO',
|
||||
source: LeadSource.EVENT,
|
||||
notes: 'Met at conference',
|
||||
};
|
||||
leadsService.create.mockResolvedValue({ ...mockLead, ...createDto });
|
||||
|
||||
await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(leadsService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a lead', async () => {
|
||||
const updateDto: UpdateLeadDto = {
|
||||
status: LeadStatus.CONTACTED,
|
||||
notes: 'Followed up via phone',
|
||||
};
|
||||
const updatedLead = { ...mockLead, ...updateDto };
|
||||
leadsService.update.mockResolvedValue(updatedLead);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockLeadId, updateDto);
|
||||
|
||||
expect(leadsService.update).toHaveBeenCalledWith(mockTenantId, mockLeadId, updateDto);
|
||||
expect(result.status).toBe(LeadStatus.CONTACTED);
|
||||
});
|
||||
|
||||
it('should update lead score', async () => {
|
||||
const updateDto: UpdateLeadDto = { score: 85 };
|
||||
leadsService.update.mockResolvedValue({ ...mockLead, score: 85 });
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockLeadId, updateDto);
|
||||
|
||||
expect(result.score).toBe(85);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should delete a lead', async () => {
|
||||
leadsService.remove.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.remove(mockRequestUser, mockLeadId);
|
||||
|
||||
expect(leadsService.remove).toHaveBeenCalledWith(mockTenantId, mockLeadId);
|
||||
expect(result).toEqual({ message: 'Lead deleted successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('convert', () => {
|
||||
it('should convert lead to opportunity', async () => {
|
||||
const convertDto: ConvertLeadDto = {
|
||||
opportunityName: 'Enterprise Deal',
|
||||
amount: 50000,
|
||||
expectedCloseDate: '2026-03-01',
|
||||
};
|
||||
const opportunityId = 'new-opportunity-id';
|
||||
leadsService.convert.mockResolvedValue({ opportunityId });
|
||||
|
||||
const result = await controller.convert(mockRequestUser, mockLeadId, convertDto);
|
||||
|
||||
expect(leadsService.convert).toHaveBeenCalledWith(mockTenantId, mockLeadId, convertDto);
|
||||
expect(result).toEqual({ opportunityId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateScore', () => {
|
||||
it('should calculate lead score', async () => {
|
||||
leadsService.calculateScore.mockResolvedValue({ score: 75 });
|
||||
|
||||
const result = await controller.calculateScore(mockRequestUser, mockLeadId);
|
||||
|
||||
expect(leadsService.calculateScore).toHaveBeenCalledWith(mockTenantId, mockLeadId);
|
||||
expect(result).toEqual({ score: 75 });
|
||||
});
|
||||
});
|
||||
});
|
||||
200
src/modules/sales/__tests__/opportunities.controller.spec.ts
Normal file
200
src/modules/sales/__tests__/opportunities.controller.spec.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { OpportunitiesController } from '../controllers/opportunities.controller';
|
||||
import { OpportunitiesService } from '../services/opportunities.service';
|
||||
import { CreateOpportunityDto, UpdateOpportunityDto, UpdateOpportunityStageDto, OpportunityListQueryDto } from '../dto';
|
||||
import { OpportunityStage } from '../entities/opportunity.entity';
|
||||
|
||||
describe('OpportunitiesController', () => {
|
||||
let controller: OpportunitiesController;
|
||||
let opportunitiesService: jest.Mocked<OpportunitiesService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockOpportunityId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockOpportunity = {
|
||||
id: mockOpportunityId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'Enterprise Deal',
|
||||
description: 'Large enterprise contract',
|
||||
stage: OpportunityStage.PROSPECTING,
|
||||
amount: 50000,
|
||||
currency: 'USD',
|
||||
probability: 25,
|
||||
expectedCloseDate: new Date('2026-03-01'),
|
||||
assignedTo: mockUserId,
|
||||
contactName: 'John Doe',
|
||||
contactEmail: 'john@example.com',
|
||||
companyName: 'Acme Corp',
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
createdBy: mockUserId,
|
||||
};
|
||||
|
||||
const mockPaginatedOpportunities = {
|
||||
data: [mockOpportunity],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
const mockPipelineSummary = [
|
||||
{ stage: OpportunityStage.PROSPECTING, count: 10, value: 100000 },
|
||||
{ stage: OpportunityStage.QUALIFICATION, count: 8, value: 80000 },
|
||||
{ stage: OpportunityStage.PROPOSAL, count: 5, value: 75000 },
|
||||
{ stage: OpportunityStage.NEGOTIATION, count: 3, value: 90000 },
|
||||
{ stage: OpportunityStage.CLOSED_WON, count: 2, value: 60000 },
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockOpportunitiesService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateStage: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
getPipelineSummary: jest.fn(),
|
||||
assignTo: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [OpportunitiesController],
|
||||
providers: [
|
||||
{
|
||||
provide: OpportunitiesService,
|
||||
useValue: mockOpportunitiesService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<OpportunitiesController>(OpportunitiesController);
|
||||
opportunitiesService = module.get(OpportunitiesService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated opportunities', async () => {
|
||||
const query: OpportunityListQueryDto = { page: 1, limit: 20 };
|
||||
opportunitiesService.findAll.mockResolvedValue(mockPaginatedOpportunities);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(opportunitiesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
expect(result).toEqual(mockPaginatedOpportunities);
|
||||
});
|
||||
|
||||
it('should filter by stage', async () => {
|
||||
const query: OpportunityListQueryDto = { page: 1, limit: 20, stage: OpportunityStage.PROPOSAL };
|
||||
opportunitiesService.findAll.mockResolvedValue({ ...mockPaginatedOpportunities, data: [] });
|
||||
|
||||
await controller.findAll(mockRequestUser, query);
|
||||
|
||||
expect(opportunitiesService.findAll).toHaveBeenCalledWith(mockTenantId, query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPipelineSummary', () => {
|
||||
it('should return pipeline summary', async () => {
|
||||
opportunitiesService.getPipelineSummary.mockResolvedValue(mockPipelineSummary);
|
||||
|
||||
const result = await controller.getPipelineSummary(mockRequestUser);
|
||||
|
||||
expect(opportunitiesService.getPipelineSummary).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockPipelineSummary);
|
||||
expect(result.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return an opportunity by id', async () => {
|
||||
opportunitiesService.findOne.mockResolvedValue(mockOpportunity);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockOpportunityId);
|
||||
|
||||
expect(opportunitiesService.findOne).toHaveBeenCalledWith(mockTenantId, mockOpportunityId);
|
||||
expect(result).toEqual(mockOpportunity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new opportunity', async () => {
|
||||
const createDto: CreateOpportunityDto = {
|
||||
name: 'New Deal',
|
||||
amount: 25000,
|
||||
expectedCloseDate: '2026-04-01',
|
||||
};
|
||||
const createdOpportunity = { ...mockOpportunity, ...createDto, id: 'new-opp-id' };
|
||||
opportunitiesService.create.mockResolvedValue(createdOpportunity);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(opportunitiesService.create).toHaveBeenCalledWith(mockTenantId, mockUserId, createDto);
|
||||
expect(result).toEqual(createdOpportunity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update an opportunity', async () => {
|
||||
const updateDto: UpdateOpportunityDto = {
|
||||
amount: 75000,
|
||||
probability: 50,
|
||||
};
|
||||
const updatedOpportunity = { ...mockOpportunity, ...updateDto };
|
||||
opportunitiesService.update.mockResolvedValue(updatedOpportunity);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockOpportunityId, updateDto);
|
||||
|
||||
expect(opportunitiesService.update).toHaveBeenCalledWith(mockTenantId, mockOpportunityId, updateDto);
|
||||
expect(result.amount).toBe(75000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStage', () => {
|
||||
it('should update opportunity stage', async () => {
|
||||
const stageDto: UpdateOpportunityStageDto = { stage: OpportunityStage.NEGOTIATION };
|
||||
const updatedOpportunity = { ...mockOpportunity, stage: OpportunityStage.NEGOTIATION, probability: 60 };
|
||||
opportunitiesService.updateStage.mockResolvedValue(updatedOpportunity);
|
||||
|
||||
const result = await controller.updateStage(mockRequestUser, mockOpportunityId, stageDto);
|
||||
|
||||
expect(opportunitiesService.updateStage).toHaveBeenCalledWith(mockTenantId, mockOpportunityId, stageDto);
|
||||
expect(result.stage).toBe(OpportunityStage.NEGOTIATION);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should delete an opportunity', async () => {
|
||||
opportunitiesService.remove.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.remove(mockRequestUser, mockOpportunityId);
|
||||
|
||||
expect(opportunitiesService.remove).toHaveBeenCalledWith(mockTenantId, mockOpportunityId);
|
||||
expect(result).toEqual({ message: 'Opportunity deleted successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('assign', () => {
|
||||
it('should assign opportunity to a user', async () => {
|
||||
const newUserId = 'new-user-id';
|
||||
const assignedOpportunity = { ...mockOpportunity, assignedTo: newUserId };
|
||||
opportunitiesService.assignTo.mockResolvedValue(assignedOpportunity);
|
||||
|
||||
const result = await controller.assign(mockRequestUser, mockOpportunityId, newUserId);
|
||||
|
||||
expect(opportunitiesService.assignTo).toHaveBeenCalledWith(mockTenantId, mockOpportunityId, newUserId);
|
||||
expect(result.assignedTo).toBe(newUserId);
|
||||
});
|
||||
});
|
||||
});
|
||||
166
src/modules/sales/__tests__/pipeline.controller.spec.ts
Normal file
166
src/modules/sales/__tests__/pipeline.controller.spec.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PipelineController } from '../controllers/pipeline.controller';
|
||||
import { PipelineService } from '../services/pipeline.service';
|
||||
import { CreatePipelineStageDto, UpdatePipelineStageDto, ReorderStagesDto } from '../dto';
|
||||
|
||||
describe('PipelineController', () => {
|
||||
let controller: PipelineController;
|
||||
let pipelineService: jest.Mocked<PipelineService>;
|
||||
|
||||
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
||||
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
||||
const mockStageId = '550e8400-e29b-41d4-a716-446655440003';
|
||||
|
||||
const mockRequestUser = {
|
||||
id: mockUserId,
|
||||
email: 'test@example.com',
|
||||
tenant_id: mockTenantId,
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const mockPipelineStage = {
|
||||
id: mockStageId,
|
||||
tenantId: mockTenantId,
|
||||
name: 'Qualification',
|
||||
code: 'qualification',
|
||||
description: 'Qualification stage',
|
||||
displayOrder: 1,
|
||||
color: '#3498db',
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
};
|
||||
|
||||
const mockPipelineStages = [
|
||||
mockPipelineStage,
|
||||
{ ...mockPipelineStage, id: 'stage-2', name: 'Proposal', code: 'proposal', displayOrder: 2 },
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockPipelineService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
reorder: jest.fn(),
|
||||
initializeDefaults: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PipelineController],
|
||||
providers: [
|
||||
{
|
||||
provide: PipelineService,
|
||||
useValue: mockPipelineService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PipelineController>(PipelineController);
|
||||
pipelineService = module.get(PipelineService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all pipeline stages', async () => {
|
||||
pipelineService.findAll.mockResolvedValue(mockPipelineStages);
|
||||
|
||||
const result = await controller.findAll(mockRequestUser);
|
||||
|
||||
expect(pipelineService.findAll).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual(mockPipelineStages);
|
||||
expect(result.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a pipeline stage by id', async () => {
|
||||
pipelineService.findOne.mockResolvedValue(mockPipelineStage);
|
||||
|
||||
const result = await controller.findOne(mockRequestUser, mockStageId);
|
||||
|
||||
expect(pipelineService.findOne).toHaveBeenCalledWith(mockTenantId, mockStageId);
|
||||
expect(result).toEqual(mockPipelineStage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new pipeline stage', async () => {
|
||||
const createDto: CreatePipelineStageDto = {
|
||||
name: 'New Stage',
|
||||
code: 'new_stage',
|
||||
displayOrder: 3,
|
||||
};
|
||||
const createdStage = { ...mockPipelineStage, ...createDto, id: 'new-stage-id' };
|
||||
pipelineService.create.mockResolvedValue(createdStage);
|
||||
|
||||
const result = await controller.create(mockRequestUser, createDto);
|
||||
|
||||
expect(pipelineService.create).toHaveBeenCalledWith(mockTenantId, createDto);
|
||||
expect(result).toEqual(createdStage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a pipeline stage', async () => {
|
||||
const updateDto: UpdatePipelineStageDto = {
|
||||
name: 'Updated Stage Name',
|
||||
color: '#e74c3c',
|
||||
};
|
||||
const updatedStage = { ...mockPipelineStage, ...updateDto };
|
||||
pipelineService.update.mockResolvedValue(updatedStage);
|
||||
|
||||
const result = await controller.update(mockRequestUser, mockStageId, updateDto);
|
||||
|
||||
expect(pipelineService.update).toHaveBeenCalledWith(mockTenantId, mockStageId, updateDto);
|
||||
expect(result.name).toBe('Updated Stage Name');
|
||||
expect(result.color).toBe('#e74c3c');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should delete a pipeline stage', async () => {
|
||||
pipelineService.remove.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.remove(mockRequestUser, mockStageId);
|
||||
|
||||
expect(pipelineService.remove).toHaveBeenCalledWith(mockTenantId, mockStageId);
|
||||
expect(result).toEqual({ message: 'Pipeline stage deleted successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorder', () => {
|
||||
it('should reorder pipeline stages', async () => {
|
||||
const reorderDto: ReorderStagesDto = {
|
||||
stageIds: ['stage-2', mockStageId],
|
||||
};
|
||||
const reorderedStages = [
|
||||
{ ...mockPipelineStages[1], displayOrder: 1 },
|
||||
{ ...mockPipelineStages[0], displayOrder: 2 },
|
||||
];
|
||||
pipelineService.reorder.mockResolvedValue(reorderedStages);
|
||||
|
||||
const result = await controller.reorder(mockRequestUser, reorderDto);
|
||||
|
||||
expect(pipelineService.reorder).toHaveBeenCalledWith(mockTenantId, reorderDto);
|
||||
expect(result[0].displayOrder).toBe(1);
|
||||
expect(result[1].displayOrder).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeDefaults', () => {
|
||||
it('should initialize default pipeline stages', async () => {
|
||||
pipelineService.initializeDefaults.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.initializeDefaults(mockRequestUser);
|
||||
|
||||
expect(pipelineService.initializeDefaults).toHaveBeenCalledWith(mockTenantId);
|
||||
expect(result).toEqual({ message: 'Default pipeline stages initialized' });
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user