diff --git a/workers/main/src/activities/weeklyFinancialReports/getTargetUnits.test.ts b/workers/main/src/activities/weeklyFinancialReports/getTargetUnits.test.ts index f8a2d24..f9eefbe 100644 --- a/workers/main/src/activities/weeklyFinancialReports/getTargetUnits.test.ts +++ b/workers/main/src/activities/weeklyFinancialReports/getTargetUnits.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { AppError } from '../../common/errors'; import { writeJsonFile } from '../../common/fileUtils'; import { RedminePool } from '../../common/RedminePool'; +import { GroupNameEnum } from '../../configs/weeklyFinancialReport'; import { getTargetUnits } from './getTargetUnits'; type TargetUnit = { @@ -80,6 +81,7 @@ vi.mock('../../common/fileUtils', () => ({ })); vi.mock('../../configs/redmineDatabase', () => ({ redmineDatabaseConfig: {}, + redmineDatabaseSchema: {}, })); describe('getTargetUnits', () => { @@ -112,10 +114,10 @@ describe('getTargetUnits', () => { })); }; - it('returns fileLink when successful', async () => { + it('returns fileLink when successful (default group)', async () => { mockRepo(true); writeJsonFileMock.mockResolvedValue(undefined); - const result = await getTargetUnits(); + const result = await getTargetUnits(GroupNameEnum.SD_REPORT); expect(result).toEqual({ fileLink: mockFile }); expect(writeJsonFile).toHaveBeenCalledWith(mockFile, mockUnits); @@ -124,8 +126,10 @@ describe('getTargetUnits', () => { it('throws AppError when repo.getTargetUnits throws', async () => { mockRepo(false); writeJsonFileMock.mockResolvedValue(undefined); - await expect(getTargetUnits()).rejects.toThrow(AppError); - await expect(getTargetUnits()).rejects.toThrow( + await expect(getTargetUnits(GroupNameEnum.SD_REPORT)).rejects.toThrow( + AppError, + ); + await expect(getTargetUnits(GroupNameEnum.SD_REPORT)).rejects.toThrow( 'Failed to get Target Units', ); }); @@ -133,8 +137,10 @@ describe('getTargetUnits', () => { it('throws AppError when writeJsonFile throws', async () => { mockRepo(true); writeJsonFileMock.mockRejectedValue(new Error('fail-write')); - await expect(getTargetUnits()).rejects.toThrow(AppError); - await expect(getTargetUnits()).rejects.toThrow( + await expect(getTargetUnits(GroupNameEnum.SD_REPORT)).rejects.toThrow( + AppError, + ); + await expect(getTargetUnits(GroupNameEnum.SD_REPORT)).rejects.toThrow( 'Failed to get Target Units', ); }); @@ -142,7 +148,7 @@ describe('getTargetUnits', () => { it('always ends the Redmine pool', async () => { mockRepo(true); writeJsonFileMock.mockResolvedValue(undefined); - await getTargetUnits(); + await getTargetUnits(GroupNameEnum.SD_REPORT); expect(endPool).toHaveBeenCalled(); }); }); diff --git a/workers/main/src/activities/weeklyFinancialReports/getTargetUnits.ts b/workers/main/src/activities/weeklyFinancialReports/getTargetUnits.ts index 99e40ed..e6deb01 100644 --- a/workers/main/src/activities/weeklyFinancialReports/getTargetUnits.ts +++ b/workers/main/src/activities/weeklyFinancialReports/getTargetUnits.ts @@ -1,6 +1,7 @@ import { AppError } from '../../common/errors'; import { writeJsonFile } from '../../common/fileUtils'; import { RedminePool } from '../../common/RedminePool'; +import { GroupName } from '../../common/types'; import { redmineDatabaseConfig } from '../../configs/redmineDatabase'; import { TargetUnitRepository } from '../../services/TargetUnit/TargetUnitRepository'; @@ -8,14 +9,16 @@ interface GetTargetUnitsResult { fileLink: string; } -export const getTargetUnits = async (): Promise => { +export const getTargetUnits = async ( + groupName: GroupName, +): Promise => { const redminePool = new RedminePool(redmineDatabaseConfig); try { const pool = redminePool.getPool(); const repo = new TargetUnitRepository(pool); - const result = await repo.getTargetUnits(); + const result = await repo.getTargetUnits(groupName); const filename = `data/weeklyFinancialReportsWorkflow/getTargetUnits/target-units-${Date.now()}.json`; await writeJsonFile(filename, result); diff --git a/workers/main/src/services/TargetUnit/ITargetUnitRepository.ts b/workers/main/src/services/TargetUnit/ITargetUnitRepository.ts index 114dfda..029afcb 100644 --- a/workers/main/src/services/TargetUnit/ITargetUnitRepository.ts +++ b/workers/main/src/services/TargetUnit/ITargetUnitRepository.ts @@ -1,5 +1,5 @@ -import { TargetUnit } from '../../common/types'; +import { GroupName, TargetUnit } from '../../common/types'; export interface ITargetUnitRepository { - getTargetUnits(): Promise; + getTargetUnits(groupName: GroupName): Promise; } diff --git a/workers/main/src/services/TargetUnit/TargetUnitRepository.test.ts b/workers/main/src/services/TargetUnit/TargetUnitRepository.test.ts index 6645904..818b5d5 100644 --- a/workers/main/src/services/TargetUnit/TargetUnitRepository.test.ts +++ b/workers/main/src/services/TargetUnit/TargetUnitRepository.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { TargetUnitRepositoryError } from '../../common/errors'; import { TargetUnit } from '../../common/types'; +import { GroupNameEnum } from '../../configs/weeklyFinancialReport'; import { TargetUnitRepository } from './TargetUnitRepository'; import { TargetUnitRow } from './types'; @@ -37,7 +38,7 @@ describe('TargetUnitRepository', () => { ]; mockPool.query.mockResolvedValueOnce([rows]); - const result = await repo.getTargetUnits(); + const result = await repo.getTargetUnits(GroupNameEnum.SD_REPORT); expect(result).toEqual[]>([ { @@ -55,13 +56,15 @@ describe('TargetUnitRepository', () => { it('throws TargetUnitRepositoryError if query does not return array', async () => { mockPool.query.mockResolvedValueOnce([null]); - await expect(repo.getTargetUnits()).rejects.toThrow( + await expect(repo.getTargetUnits(GroupNameEnum.SD_REPORT)).rejects.toThrow( TargetUnitRepositoryError, ); }); it('throws TargetUnitRepositoryError on query error', async () => { mockPool.query.mockRejectedValueOnce(new Error('db error')); - await expect(repo.getTargetUnits()).rejects.toThrowError('db error'); + await expect( + repo.getTargetUnits(GroupNameEnum.SD_REPORT), + ).rejects.toThrowError('db error'); }); }); diff --git a/workers/main/src/services/TargetUnit/TargetUnitRepository.ts b/workers/main/src/services/TargetUnit/TargetUnitRepository.ts index f407de7..210ebbd 100644 --- a/workers/main/src/services/TargetUnit/TargetUnitRepository.ts +++ b/workers/main/src/services/TargetUnit/TargetUnitRepository.ts @@ -1,7 +1,7 @@ import { Pool } from 'mysql2/promise'; import { TargetUnitRepositoryError } from '../../common/errors'; -import { TargetUnit } from '../../common/types'; +import { GroupName, TargetUnit } from '../../common/types'; import { ITargetUnitRepository } from './ITargetUnitRepository'; import { TARGET_UNITS_QUERY } from './queries'; import { TargetUnitRow } from './types'; @@ -33,9 +33,12 @@ export class TargetUnitRepository implements ITargetUnitRepository { total_hours: Number(total_hours), }); - async getTargetUnits(): Promise { + async getTargetUnits(groupName: GroupName): Promise { try { - const [rows] = await this.pool.query(TARGET_UNITS_QUERY); + const [rows] = await this.pool.query( + TARGET_UNITS_QUERY, + [groupName], + ); if (!Array.isArray(rows)) { throw new TargetUnitRepositoryError('Query did not return an array'); diff --git a/workers/main/src/services/TargetUnit/queries.ts b/workers/main/src/services/TargetUnit/queries.ts index 7b91e22..ad9f2c6 100644 --- a/workers/main/src/services/TargetUnit/queries.ts +++ b/workers/main/src/services/TargetUnit/queries.ts @@ -1,41 +1,43 @@ export const TARGET_UNITS_QUERY = `SELECT - group_id, - group_name, - project_id, - project_name, - user_id, - username, - DATE_FORMAT(spent_on, '%Y-%m-%d') AS spent_on, - SUM(total_hours) AS total_hours -FROM ( - SELECT - g.id AS group_id, - g.lastname AS group_name, - p.id AS project_id, - p.name AS project_name, - te.user_id AS user_id, - CONCAT(u.firstname, ' ', u.lastname) AS username, - te.spent_on AS spent_on, - te.hours AS total_hours - FROM users AS g - JOIN members AS m ON m.user_id = g.id - JOIN projects AS p ON p.id = m.project_id - JOIN time_entries te ON te.project_id = p.id - JOIN users AS u ON u.id = te.user_id - WHERE g.type = 'Group' - AND te.spent_on BETWEEN DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) + 7 DAY) - AND DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) + 1 DAY) -) t -GROUP BY - group_id, - group_name, - project_id, - project_name, - user_id, - username, - spent_on -ORDER BY - group_name ASC, - project_name ASC, - username ASC, - spent_on ASC`; + group_id, + group_name, + project_id, + project_name, + user_id, + username, + DATE_FORMAT(spent_on, '%Y-%m-%d') AS spent_on, + SUM(total_hours) AS total_hours + FROM ( + SELECT + g.id AS group_id, + g.lastname AS group_name, + p.id AS project_id, + p.name AS project_name, + te.user_id AS user_id, + CONCAT(u.firstname, ' ', u.lastname) AS username, + te.spent_on AS spent_on, + te.hours AS total_hours + FROM users AS g + JOIN custom_values as cv ON cv.customized_id = g.id + JOIN members AS m ON m.user_id = g.id + JOIN projects AS p ON p.id = m.project_id + JOIN time_entries te ON te.project_id = p.id + JOIN users AS u ON u.id = te.user_id + WHERE g.type = 'Group' + AND cv.customized_type = 'Principal' and cv.value = ? + AND te.spent_on BETWEEN DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) + 7 DAY) + AND DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) + 1 DAY) + ) t + GROUP BY + group_id, + group_name, + project_id, + project_name, + user_id, + username, + spent_on + ORDER BY + group_name ASC, + project_name ASC, + username ASC, + spent_on ASC`; diff --git a/workers/main/src/workflows/weeklyFinancialReports/weeklyFinancialReports.workflow.test.ts b/workers/main/src/workflows/weeklyFinancialReports/weeklyFinancialReports.workflow.test.ts index ef37364..3855722 100644 --- a/workers/main/src/workflows/weeklyFinancialReports/weeklyFinancialReports.workflow.test.ts +++ b/workers/main/src/workflows/weeklyFinancialReports/weeklyFinancialReports.workflow.test.ts @@ -1,25 +1,83 @@ -import { describe, expect, it, vi } from 'vitest'; +import * as workflowModule from '@temporalio/workflow'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { AppError } from '../../common/errors'; import { GroupNameEnum } from '../../configs/weeklyFinancialReport'; import { weeklyFinancialReportsWorkflow } from './weeklyFinancialReports.workflow'; -vi.mock('@temporalio/workflow', () => ({ - proxyActivities: () => ({ - getTargetUnits: vi - .fn() - .mockResolvedValue({ fileLink: 'sub-dir/mocked-link.json' }), - fetchFinancialAppData: vi - .fn() - .mockResolvedValue({ fileLink: 'sub-dir/mocked-link.json' }), - }), -})); +vi.mock('@temporalio/workflow', () => { + const getTargetUnitsMock = vi.fn(); + const fetchFinancialAppDataMock = vi.fn(); + + return { + proxyActivities: () => ({ + getTargetUnits: getTargetUnitsMock, + fetchFinancialAppData: fetchFinancialAppDataMock, + }), + __getTargetUnitsMock: () => getTargetUnitsMock, + __getFetchFinancialAppDataMock: () => fetchFinancialAppDataMock, + }; +}); describe('weeklyFinancialReportsWorkflow', () => { - it('returns the fileLink from getTargetUnits', async () => { + type WorkflowModuleWithMock = typeof workflowModule & { + __getTargetUnitsMock: () => ReturnType; + __getFetchFinancialAppDataMock: () => ReturnType; + }; + const getTargetUnitsMock = ( + workflowModule as WorkflowModuleWithMock + ).__getTargetUnitsMock(); + const fetchFinancialAppDataMock = ( + workflowModule as WorkflowModuleWithMock + ).__getFetchFinancialAppDataMock(); + + beforeEach(() => { + getTargetUnitsMock.mockReset(); + fetchFinancialAppDataMock.mockReset(); + }); + + it('throws AppError for invalid group name', async () => { + const allowedValues = Object.values(GroupNameEnum).join('", "'); + const expectedMessage = `Invalid groupName parameter: INVALID_GROUP. Allowed values: "${allowedValues}"`; + + await expect( + weeklyFinancialReportsWorkflow( + 'INVALID_GROUP' as unknown as GroupNameEnum, + ), + ).rejects.toThrow(AppError); + await expect( + weeklyFinancialReportsWorkflow( + 'INVALID_GROUP' as unknown as GroupNameEnum, + ), + ).rejects.toThrow(expectedMessage); + }); + + it('propagates error from getTargetUnits', async () => { + getTargetUnitsMock.mockRejectedValueOnce(new Error('activity error')); + await expect( + weeklyFinancialReportsWorkflow(GroupNameEnum.SD_REPORT), + ).rejects.toThrow('activity error'); + }); + + it('propagates error from fetchFinancialAppData', async () => { + getTargetUnitsMock.mockResolvedValueOnce({ fileLink: 'file.json' }); + fetchFinancialAppDataMock.mockRejectedValueOnce(new Error('fetch error')); + await expect( + weeklyFinancialReportsWorkflow(GroupNameEnum.SD_REPORT), + ).rejects.toThrow('fetch error'); + }); + + it('returns fileLink on success', async () => { + getTargetUnitsMock.mockResolvedValueOnce({ fileLink: 'file.json' }); + fetchFinancialAppDataMock.mockResolvedValueOnce({ + fileLink: 'result.json', + }); const result = await weeklyFinancialReportsWorkflow( - GroupNameEnum.ED_REPORT, + GroupNameEnum.SD_REPORT, ); - expect(result).toBe('sub-dir/mocked-link.json'); + expect(result).toBe('result.json'); + expect(getTargetUnitsMock).toHaveBeenCalledWith(GroupNameEnum.SD_REPORT); + expect(fetchFinancialAppDataMock).toHaveBeenCalledWith('file.json'); }); }); diff --git a/workers/main/src/workflows/weeklyFinancialReports/weeklyFinancialReports.workflow.ts b/workers/main/src/workflows/weeklyFinancialReports/weeklyFinancialReports.workflow.ts index feb8394..1e4a5aa 100644 --- a/workers/main/src/workflows/weeklyFinancialReports/weeklyFinancialReports.workflow.ts +++ b/workers/main/src/workflows/weeklyFinancialReports/weeklyFinancialReports.workflow.ts @@ -16,11 +16,11 @@ export async function weeklyFinancialReportsWorkflow( ): Promise { if (!(Object.values(GroupNameEnum) as GroupName[]).includes(groupName)) { throw new AppError( - `Invalid groupName paramter: ${groupName}. Allowed values: "${Object.values(GroupNameEnum).join('", "')}"`, + `Invalid groupName parameter: ${groupName}. Allowed values: "${Object.values(GroupNameEnum).join('", "')}"`, 'weeklyFinancialReportsWorkflow', ); } - const targetUnits = await getTargetUnits(); + const targetUnits = await getTargetUnits(groupName); const finData = await fetchFinancialAppData(targetUnits.fileLink); return finData.fileLink;