[TASK-009] test: Add CRM integration and unit tests
- Add crm-flow.integration.test.ts with complete CRM workflow tests: - Lead creation and qualification flow - Lead to opportunity conversion - Opportunity pipeline stage progression - Opportunity won/lost handling - Activities and follow-up tracking - Edge cases and error handling - Add activities.service.test.ts with unit tests for: - CRUD operations for activities - Activity status transitions (scheduled/done/cancelled) - Resource linking (leads, opportunities, partners) - Activity summary and overdue tracking - Follow-up scheduling - Update helpers.ts with createMockActivity factory Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
23631d3b9b
commit
5ee2023428
@ -557,6 +557,35 @@ export function createMockTimesheet(overrides: Record<string, any> = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// Activity factory (CRM)
|
||||
export function createMockActivity(overrides: Record<string, any> = {}) {
|
||||
return {
|
||||
id: 'activity-uuid-1',
|
||||
tenant_id: global.testTenantId,
|
||||
company_id: 'company-uuid-1',
|
||||
company_name: 'Test Company',
|
||||
activity_type: 'call' as const,
|
||||
name: 'Test Activity',
|
||||
description: null,
|
||||
user_id: 'user-uuid-1',
|
||||
user_name: 'John Doe',
|
||||
res_model: 'opportunity',
|
||||
res_id: 'opportunity-uuid-1',
|
||||
res_name: 'Test Opportunity',
|
||||
partner_id: 'partner-uuid-1',
|
||||
partner_name: 'Test Partner',
|
||||
scheduled_date: new Date(),
|
||||
date_done: null,
|
||||
duration_hours: 1,
|
||||
status: 'scheduled' as const,
|
||||
priority: 1,
|
||||
notes: null,
|
||||
created_at: new Date(),
|
||||
created_by: 'user-uuid-1',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// Core Catalog Factories
|
||||
// =====================================================
|
||||
|
||||
647
src/modules/crm/__tests__/activities.service.test.ts
Normal file
647
src/modules/crm/__tests__/activities.service.test.ts
Normal file
@ -0,0 +1,647 @@
|
||||
/**
|
||||
* Activities Service Unit Tests
|
||||
*
|
||||
* Tests for CRM activities (calls, meetings, emails, tasks, notes)
|
||||
*/
|
||||
|
||||
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
|
||||
import { createMockActivity } from '../../../__tests__/helpers.js';
|
||||
|
||||
// Mock query functions
|
||||
const mockQuery = jest.fn();
|
||||
const mockQueryOne = jest.fn();
|
||||
const mockGetClient = jest.fn();
|
||||
|
||||
jest.mock('../../../config/database.js', () => ({
|
||||
query: (...args: any[]) => mockQuery(...args),
|
||||
queryOne: (...args: any[]) => mockQueryOne(...args),
|
||||
getClient: () => mockGetClient(),
|
||||
}));
|
||||
|
||||
// Mock logger
|
||||
jest.mock('../../../shared/utils/logger.js', () => ({
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Import after mocking
|
||||
import { activitiesService } from '../activities.service.js';
|
||||
import { NotFoundError, ValidationError } from '../../../shared/errors/index.js';
|
||||
|
||||
describe('ActivitiesService', () => {
|
||||
const tenantId = 'test-tenant-uuid';
|
||||
const userId = 'test-user-uuid';
|
||||
const companyId = 'company-uuid-1';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return activities with pagination', async () => {
|
||||
const mockActivities = [
|
||||
createMockActivity({ id: '1', name: 'Call 1' }),
|
||||
createMockActivity({ id: '2', name: 'Meeting 1' }),
|
||||
];
|
||||
|
||||
mockQueryOne.mockResolvedValue({ count: '2' });
|
||||
mockQuery.mockResolvedValue(mockActivities);
|
||||
|
||||
const result = await activitiesService.findAll(tenantId, { page: 1, limit: 20 });
|
||||
|
||||
expect(result.data).toHaveLength(2);
|
||||
expect(result.total).toBe(2);
|
||||
});
|
||||
|
||||
it('should filter by activity_type', async () => {
|
||||
mockQueryOne.mockResolvedValue({ count: '0' });
|
||||
mockQuery.mockResolvedValue([]);
|
||||
|
||||
await activitiesService.findAll(tenantId, { activity_type: 'call' });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('a.activity_type = $'),
|
||||
expect.arrayContaining([tenantId, 'call'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
mockQueryOne.mockResolvedValue({ count: '0' });
|
||||
mockQuery.mockResolvedValue([]);
|
||||
|
||||
await activitiesService.findAll(tenantId, { status: 'scheduled' });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('a.status = $'),
|
||||
expect.arrayContaining([tenantId, 'scheduled'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by res_model and res_id', async () => {
|
||||
mockQueryOne.mockResolvedValue({ count: '0' });
|
||||
mockQuery.mockResolvedValue([]);
|
||||
|
||||
await activitiesService.findAll(tenantId, { res_model: 'opportunity', res_id: 'opp-uuid-1' });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('a.res_model = $'),
|
||||
expect.arrayContaining([tenantId, 'opportunity', 'opp-uuid-1'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by date range', async () => {
|
||||
mockQueryOne.mockResolvedValue({ count: '0' });
|
||||
mockQuery.mockResolvedValue([]);
|
||||
|
||||
await activitiesService.findAll(tenantId, {
|
||||
date_from: '2026-01-01',
|
||||
date_to: '2026-01-31',
|
||||
});
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('a.scheduled_date >= $'),
|
||||
expect.any(Array)
|
||||
);
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('a.scheduled_date <= $'),
|
||||
expect.any(Array)
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by search term', async () => {
|
||||
mockQueryOne.mockResolvedValue({ count: '0' });
|
||||
mockQuery.mockResolvedValue([]);
|
||||
|
||||
await activitiesService.findAll(tenantId, { search: 'demo' });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('a.name ILIKE'),
|
||||
expect.arrayContaining([tenantId, '%demo%'])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return activity when found', async () => {
|
||||
const mockActivity = createMockActivity();
|
||||
mockQueryOne.mockResolvedValue(mockActivity);
|
||||
|
||||
const result = await activitiesService.findById('activity-uuid-1', tenantId);
|
||||
|
||||
expect(result).toEqual(mockActivity);
|
||||
});
|
||||
|
||||
it('should throw NotFoundError when activity not found', async () => {
|
||||
mockQueryOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
activitiesService.findById('nonexistent-id', tenantId)
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('should fetch resource name for linked activity', async () => {
|
||||
const mockActivity = createMockActivity({
|
||||
res_model: 'opportunity',
|
||||
res_id: 'opp-uuid-1',
|
||||
});
|
||||
|
||||
mockQueryOne
|
||||
.mockResolvedValueOnce(mockActivity) // findById
|
||||
.mockResolvedValueOnce({ name: 'Big Deal Opportunity' }); // getResourceName
|
||||
|
||||
const result = await activitiesService.findById('activity-uuid-1', tenantId);
|
||||
|
||||
expect(result.res_name).toBe('Big Deal Opportunity');
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
const createDto = {
|
||||
company_id: companyId,
|
||||
activity_type: 'call' as const,
|
||||
name: 'Initial call with prospect',
|
||||
res_model: 'lead',
|
||||
res_id: 'lead-uuid-1',
|
||||
user_id: userId,
|
||||
};
|
||||
|
||||
it('should create activity successfully', async () => {
|
||||
const createdActivity = createMockActivity({ ...createDto });
|
||||
mockQueryOne
|
||||
.mockResolvedValueOnce(createdActivity) // INSERT
|
||||
.mockResolvedValueOnce(createdActivity); // findById
|
||||
mockQuery.mockResolvedValueOnce([]); // Update last activity date
|
||||
|
||||
const result = await activitiesService.create(createDto, tenantId, userId);
|
||||
|
||||
expect(result.name).toBe(createDto.name);
|
||||
expect(result.activity_type).toBe('call');
|
||||
});
|
||||
|
||||
it('should create activity with scheduled date', async () => {
|
||||
const scheduledDate = new Date().toISOString();
|
||||
const dto = { ...createDto, scheduled_date: scheduledDate };
|
||||
const createdActivity = createMockActivity({ ...dto });
|
||||
|
||||
mockQueryOne
|
||||
.mockResolvedValueOnce(createdActivity)
|
||||
.mockResolvedValueOnce(createdActivity);
|
||||
mockQuery.mockResolvedValueOnce([]);
|
||||
|
||||
await activitiesService.create(dto, tenantId, userId);
|
||||
|
||||
expect(mockQueryOne).toHaveBeenCalledWith(
|
||||
expect.stringContaining('INSERT INTO crm.activities'),
|
||||
expect.arrayContaining([scheduledDate])
|
||||
);
|
||||
});
|
||||
|
||||
it('should update last activity date on linked resource', async () => {
|
||||
const dto = {
|
||||
...createDto,
|
||||
res_model: 'opportunity',
|
||||
res_id: 'opp-uuid-1',
|
||||
};
|
||||
const createdActivity = createMockActivity(dto);
|
||||
|
||||
mockQueryOne
|
||||
.mockResolvedValueOnce(createdActivity)
|
||||
.mockResolvedValueOnce(createdActivity);
|
||||
mockQuery.mockResolvedValueOnce([]);
|
||||
|
||||
await activitiesService.create(dto, tenantId, userId);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE crm.opportunities SET date_last_activity'),
|
||||
expect.arrayContaining(['opp-uuid-1', tenantId])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update activity successfully', async () => {
|
||||
const existingActivity = createMockActivity({ status: 'scheduled' });
|
||||
mockQueryOne.mockResolvedValue(existingActivity);
|
||||
mockQuery.mockResolvedValue([]);
|
||||
|
||||
await activitiesService.update(
|
||||
'activity-uuid-1',
|
||||
{ name: 'Updated Activity Name' },
|
||||
tenantId,
|
||||
userId
|
||||
);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE crm.activities SET'),
|
||||
expect.any(Array)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ValidationError when activity is done', async () => {
|
||||
const doneActivity = createMockActivity({ status: 'done' });
|
||||
mockQueryOne.mockResolvedValue(doneActivity);
|
||||
|
||||
await expect(
|
||||
activitiesService.update('activity-uuid-1', { name: 'New name' }, tenantId, userId)
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('should return existing activity when no fields to update', async () => {
|
||||
const existingActivity = createMockActivity({ status: 'scheduled' });
|
||||
mockQueryOne.mockResolvedValue(existingActivity);
|
||||
|
||||
const result = await activitiesService.update('activity-uuid-1', {}, tenantId, userId);
|
||||
|
||||
expect(result).toEqual(existingActivity);
|
||||
expect(mockQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('markDone', () => {
|
||||
it('should mark activity as done', async () => {
|
||||
const scheduledActivity = createMockActivity({ status: 'scheduled', res_model: null, res_id: null });
|
||||
const doneActivity = createMockActivity({ ...scheduledActivity, status: 'done' });
|
||||
|
||||
mockQueryOne
|
||||
.mockResolvedValueOnce(scheduledActivity) // findById before update
|
||||
.mockResolvedValueOnce(doneActivity); // findById after update
|
||||
mockQuery.mockResolvedValueOnce([]); // UPDATE activity
|
||||
|
||||
await activitiesService.markDone('activity-uuid-1', tenantId, userId);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining("status = 'done'"),
|
||||
expect.any(Array)
|
||||
);
|
||||
});
|
||||
|
||||
it('should add notes when marking as done', async () => {
|
||||
const scheduledActivity = createMockActivity({ status: 'scheduled' });
|
||||
mockQueryOne.mockResolvedValue(scheduledActivity);
|
||||
mockQuery.mockResolvedValueOnce([]);
|
||||
mockQuery.mockResolvedValueOnce([]);
|
||||
|
||||
await activitiesService.markDone(
|
||||
'activity-uuid-1',
|
||||
tenantId,
|
||||
userId,
|
||||
'Great conversation about the product'
|
||||
);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('notes = COALESCE($1, notes)'),
|
||||
expect.arrayContaining(['Great conversation about the product'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ValidationError when activity is already done', async () => {
|
||||
const doneActivity = createMockActivity({ status: 'done' });
|
||||
mockQueryOne.mockResolvedValue(doneActivity);
|
||||
|
||||
await expect(
|
||||
activitiesService.markDone('activity-uuid-1', tenantId, userId)
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('should throw ValidationError when activity is cancelled', async () => {
|
||||
const cancelledActivity = createMockActivity({ status: 'cancelled' });
|
||||
mockQueryOne.mockResolvedValue(cancelledActivity);
|
||||
|
||||
await expect(
|
||||
activitiesService.markDone('activity-uuid-1', tenantId, userId)
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', () => {
|
||||
it('should cancel scheduled activity', async () => {
|
||||
const scheduledActivity = createMockActivity({ status: 'scheduled', res_model: null, res_id: null });
|
||||
const cancelledActivity = createMockActivity({ ...scheduledActivity, status: 'cancelled' });
|
||||
|
||||
mockQueryOne
|
||||
.mockResolvedValueOnce(scheduledActivity) // findById before cancel
|
||||
.mockResolvedValueOnce(cancelledActivity); // findById after cancel
|
||||
mockQuery.mockResolvedValueOnce([]);
|
||||
|
||||
await activitiesService.cancel('activity-uuid-1', tenantId, userId);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining("status = 'cancelled'"),
|
||||
expect.any(Array)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ValidationError when activity is done', async () => {
|
||||
const doneActivity = createMockActivity({ status: 'done' });
|
||||
mockQueryOne.mockResolvedValue(doneActivity);
|
||||
|
||||
await expect(
|
||||
activitiesService.cancel('activity-uuid-1', tenantId, userId)
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('should throw ValidationError when activity is already cancelled', async () => {
|
||||
const cancelledActivity = createMockActivity({ status: 'cancelled' });
|
||||
mockQueryOne.mockResolvedValue(cancelledActivity);
|
||||
|
||||
await expect(
|
||||
activitiesService.cancel('activity-uuid-1', tenantId, userId)
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete scheduled activity', async () => {
|
||||
const scheduledActivity = createMockActivity({ status: 'scheduled' });
|
||||
mockQueryOne.mockResolvedValue(scheduledActivity);
|
||||
mockQuery.mockResolvedValue([]);
|
||||
|
||||
await activitiesService.delete('activity-uuid-1', tenantId);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('DELETE FROM crm.activities'),
|
||||
expect.any(Array)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ValidationError when trying to delete done activity', async () => {
|
||||
const doneActivity = createMockActivity({ status: 'done' });
|
||||
mockQueryOne.mockResolvedValue(doneActivity);
|
||||
|
||||
await expect(
|
||||
activitiesService.delete('activity-uuid-1', tenantId)
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResourceActivities', () => {
|
||||
it('should return activities for opportunity', async () => {
|
||||
const activities = [
|
||||
createMockActivity({ id: '1', name: 'Call 1', activity_type: 'call' }),
|
||||
createMockActivity({ id: '2', name: 'Meeting 1', activity_type: 'meeting' }),
|
||||
];
|
||||
mockQuery.mockResolvedValueOnce(activities);
|
||||
|
||||
const result = await activitiesService.getResourceActivities(
|
||||
'opportunity',
|
||||
'opp-uuid-1',
|
||||
tenantId
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('a.res_model = $1 AND a.res_id = $2'),
|
||||
expect.arrayContaining(['opportunity', 'opp-uuid-1', tenantId])
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by status when provided', async () => {
|
||||
mockQuery.mockResolvedValue([]);
|
||||
|
||||
await activitiesService.getResourceActivities(
|
||||
'lead',
|
||||
'lead-uuid-1',
|
||||
tenantId,
|
||||
'scheduled'
|
||||
);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('a.status = $4'),
|
||||
expect.arrayContaining(['lead', 'lead-uuid-1', tenantId, 'scheduled'])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActivitySummary', () => {
|
||||
it('should return activity summary', async () => {
|
||||
mockQueryOne.mockResolvedValue({
|
||||
total: '15',
|
||||
scheduled: '8',
|
||||
done: '5',
|
||||
cancelled: '2',
|
||||
overdue: '3',
|
||||
});
|
||||
|
||||
mockQuery.mockResolvedValue([
|
||||
{ activity_type: 'call', count: '6' },
|
||||
{ activity_type: 'meeting', count: '4' },
|
||||
{ activity_type: 'email', count: '3' },
|
||||
{ activity_type: 'task', count: '2' },
|
||||
]);
|
||||
|
||||
const result = await activitiesService.getActivitySummary(tenantId);
|
||||
|
||||
expect(result.total_activities).toBe(15);
|
||||
expect(result.scheduled).toBe(8);
|
||||
expect(result.done).toBe(5);
|
||||
expect(result.overdue).toBe(3);
|
||||
expect(result.by_type.call).toBe(6);
|
||||
expect(result.by_type.meeting).toBe(4);
|
||||
});
|
||||
|
||||
it('should filter by user when provided', async () => {
|
||||
mockQueryOne.mockResolvedValue({
|
||||
total: '5',
|
||||
scheduled: '3',
|
||||
done: '2',
|
||||
cancelled: '0',
|
||||
overdue: '1',
|
||||
});
|
||||
mockQuery.mockResolvedValue([]);
|
||||
|
||||
await activitiesService.getActivitySummary(tenantId, userId);
|
||||
|
||||
expect(mockQueryOne).toHaveBeenCalledWith(
|
||||
expect.stringContaining('user_id = $'),
|
||||
expect.arrayContaining([tenantId, userId])
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by date range', async () => {
|
||||
mockQueryOne.mockResolvedValue({
|
||||
total: '10',
|
||||
scheduled: '5',
|
||||
done: '5',
|
||||
cancelled: '0',
|
||||
overdue: '0',
|
||||
});
|
||||
mockQuery.mockResolvedValue([]);
|
||||
|
||||
await activitiesService.getActivitySummary(
|
||||
tenantId,
|
||||
undefined,
|
||||
'2026-01-01',
|
||||
'2026-01-31'
|
||||
);
|
||||
|
||||
expect(mockQueryOne).toHaveBeenCalledWith(
|
||||
expect.stringContaining('scheduled_date >= $'),
|
||||
expect.any(Array)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scheduleFollowUp', () => {
|
||||
it('should schedule follow-up inheriting resource info', async () => {
|
||||
// No res_model on completed activity to avoid getResourceName call
|
||||
const completedActivity = createMockActivity({
|
||||
id: 'completed-uuid',
|
||||
status: 'done',
|
||||
res_model: null,
|
||||
res_id: null,
|
||||
partner_id: 'partner-uuid-1',
|
||||
});
|
||||
|
||||
const followUpDto = {
|
||||
company_id: companyId,
|
||||
activity_type: 'meeting' as const,
|
||||
name: 'Follow-up meeting',
|
||||
scheduled_date: new Date().toISOString(),
|
||||
res_model: 'opportunity',
|
||||
res_id: 'opp-uuid-1',
|
||||
};
|
||||
|
||||
const newActivity = createMockActivity({
|
||||
activity_type: 'meeting',
|
||||
name: 'Follow-up meeting',
|
||||
res_model: 'opportunity',
|
||||
res_id: 'opp-uuid-1',
|
||||
partner_id: 'partner-uuid-1',
|
||||
});
|
||||
|
||||
// Mock sequence:
|
||||
// 1. findById(completedActivity) - no res_model, so no getResourceName
|
||||
// 2. create:
|
||||
// - INSERT returns newActivity
|
||||
// - findById(newActivity) - has res_model, so calls getResourceName
|
||||
mockQueryOne
|
||||
.mockResolvedValueOnce(completedActivity) // findById for completed (no getResourceName since res_model is null)
|
||||
.mockResolvedValueOnce(newActivity) // INSERT new activity
|
||||
.mockResolvedValueOnce(newActivity) // findById for new activity
|
||||
.mockResolvedValueOnce({ name: 'Test Opportunity' }); // getResourceName for new activity
|
||||
mockQuery.mockResolvedValueOnce([]); // Update last activity date
|
||||
|
||||
const result = await activitiesService.scheduleFollowUp(
|
||||
'completed-uuid',
|
||||
followUpDto,
|
||||
tenantId,
|
||||
userId
|
||||
);
|
||||
|
||||
expect(result.res_model).toBe('opportunity');
|
||||
expect(result.res_id).toBe('opp-uuid-1');
|
||||
expect(result.partner_id).toBe('partner-uuid-1');
|
||||
});
|
||||
|
||||
it('should allow overriding resource info in follow-up', async () => {
|
||||
// No res_model to simplify mock sequence
|
||||
const completedActivity = createMockActivity({
|
||||
id: 'completed-uuid',
|
||||
res_model: null,
|
||||
res_id: null,
|
||||
});
|
||||
|
||||
const followUpDto = {
|
||||
company_id: companyId,
|
||||
activity_type: 'call' as const,
|
||||
name: 'New opportunity call',
|
||||
res_model: 'opportunity',
|
||||
res_id: 'new-opp-uuid',
|
||||
};
|
||||
|
||||
const newActivity = createMockActivity({
|
||||
activity_type: 'call',
|
||||
name: 'New opportunity call',
|
||||
res_model: 'opportunity',
|
||||
res_id: 'new-opp-uuid',
|
||||
});
|
||||
|
||||
mockQueryOne
|
||||
.mockResolvedValueOnce(completedActivity) // findById completed (no getResourceName)
|
||||
.mockResolvedValueOnce(newActivity) // INSERT
|
||||
.mockResolvedValueOnce(newActivity) // findById new
|
||||
.mockResolvedValueOnce({ name: 'New Opportunity' }); // getResourceName
|
||||
mockQuery.mockResolvedValueOnce([]); // Update last activity
|
||||
|
||||
const result = await activitiesService.scheduleFollowUp(
|
||||
'completed-uuid',
|
||||
followUpDto,
|
||||
tenantId,
|
||||
userId
|
||||
);
|
||||
|
||||
expect(result.res_model).toBe('opportunity');
|
||||
expect(result.res_id).toBe('new-opp-uuid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOverdueCount', () => {
|
||||
it('should return count of overdue activities', async () => {
|
||||
mockQueryOne.mockResolvedValueOnce({ count: '7' });
|
||||
|
||||
const result = await activitiesService.getOverdueCount(tenantId);
|
||||
|
||||
expect(result).toBe(7);
|
||||
expect(mockQueryOne).toHaveBeenCalledWith(
|
||||
expect.stringContaining("status = 'scheduled'"),
|
||||
expect.arrayContaining([tenantId])
|
||||
);
|
||||
expect(mockQueryOne).toHaveBeenCalledWith(
|
||||
expect.stringContaining('scheduled_date < CURRENT_DATE'),
|
||||
expect.any(Array)
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by user when provided', async () => {
|
||||
mockQueryOne.mockResolvedValueOnce({ count: '3' });
|
||||
|
||||
await activitiesService.getOverdueCount(tenantId, userId);
|
||||
|
||||
expect(mockQueryOne).toHaveBeenCalledWith(
|
||||
expect.stringContaining('user_id = $2'),
|
||||
expect.arrayContaining([tenantId, userId])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Activity Types', () => {
|
||||
it.each([
|
||||
['call', 'Initial sales call'],
|
||||
['meeting', 'Product demo'],
|
||||
['email', 'Follow-up email'],
|
||||
['task', 'Prepare proposal'],
|
||||
['note', 'Customer feedback'],
|
||||
['other', 'Site visit'],
|
||||
])('should create %s activity', async (activityType, name) => {
|
||||
// Clear mocks before each iteration
|
||||
jest.clearAllMocks();
|
||||
|
||||
const dto = {
|
||||
company_id: companyId,
|
||||
activity_type: activityType as any,
|
||||
name,
|
||||
res_model: null,
|
||||
res_id: null,
|
||||
};
|
||||
|
||||
const createdActivity = createMockActivity({
|
||||
...dto,
|
||||
activity_type: activityType as any,
|
||||
name,
|
||||
});
|
||||
|
||||
mockQueryOne
|
||||
.mockResolvedValueOnce(createdActivity) // INSERT
|
||||
.mockResolvedValueOnce(createdActivity); // findById (no getResourceName since res_model is null)
|
||||
|
||||
const result = await activitiesService.create(dto, tenantId, userId);
|
||||
|
||||
expect(result.activity_type).toBe(activityType);
|
||||
expect(result.name).toBe(name);
|
||||
});
|
||||
});
|
||||
});
|
||||
1022
src/modules/crm/__tests__/crm-flow.integration.test.ts
Normal file
1022
src/modules/crm/__tests__/crm-flow.integration.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user