diff --git a/workers/main/src/activities/weeklyFinancialReports/index.ts b/workers/main/src/activities/weeklyFinancialReports/index.ts index 8a800f7..7ba3212 100644 --- a/workers/main/src/activities/weeklyFinancialReports/index.ts +++ b/workers/main/src/activities/weeklyFinancialReports/index.ts @@ -1,2 +1,3 @@ export * from './fetchFinancialAppData'; export * from './getTargetUnits'; +export * from './sendReportToSlack'; diff --git a/workers/main/src/activities/weeklyFinancialReports/sendReportToSlack.test.ts b/workers/main/src/activities/weeklyFinancialReports/sendReportToSlack.test.ts new file mode 100644 index 0000000..2b02cad --- /dev/null +++ b/workers/main/src/activities/weeklyFinancialReports/sendReportToSlack.test.ts @@ -0,0 +1,141 @@ +import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; + +import { AppError } from '../../common/errors'; +import { readJsonFile } from '../../common/fileUtils'; +import { TargetUnit } from '../../common/types'; +import { FinancialsAppData } from '../../services/FinApp'; +import { SlackService } from '../../services/SlackService'; +import { WeeklyFinancialReportRepository } from '../../services/WeeklyFinancialReport'; +import { sendReportToSlack } from './sendReportToSlack'; + +vi.mock('../../common/fileUtils', () => ({ + readJsonFile: vi.fn(), +})); +vi.mock('../../services/WeeklyFinancialReport', () => ({ + WeeklyFinancialReportRepository: vi.fn(), +})); +vi.mock('../../services/SlackService', () => ({ + SlackService: vi.fn(), +})); + +const mockTargetUnits: TargetUnit[] = [ + { + group_id: 1, + group_name: 'Group', + project_id: 2, + project_name: 'Project', + user_id: 3, + username: 'User', + spent_on: '2024-06-01', + total_hours: 8, + }, +]; +const mockFinancialsAppData: FinancialsAppData = { + employees: [{ redmine_id: 3, history: { rate: { '2024-06-01': 100 } } }], + projects: [{ redmine_id: 2, history: { rate: { '2024-06-01': 200 } } }], +}; + +describe('sendReportToSlack', () => { + let readJsonFileMock: Mock; + let generateReportMock: Mock; + let postMessageMock: Mock; + + function tryMockReset(obj: unknown) { + if ( + typeof obj === 'function' && + 'mockReset' in obj && + typeof (obj as { mockReset: unknown }).mockReset === 'function' + ) { + (obj as { mockReset: () => void }).mockReset(); + } + } + + beforeEach(() => { + readJsonFileMock = vi.mocked(readJsonFile); + generateReportMock = vi.fn(); + postMessageMock = vi.fn(); + + tryMockReset(WeeklyFinancialReportRepository); + tryMockReset(SlackService); + }); + + it('sends report to Slack and returns success message', async () => { + readJsonFileMock + .mockResolvedValueOnce(mockTargetUnits) + .mockResolvedValueOnce(mockFinancialsAppData); + generateReportMock.mockReturnValue({ + details: 'details', + summary: 'summary', + }); + (WeeklyFinancialReportRepository as unknown as Mock).mockImplementation( + () => ({ + generateReport: generateReportMock, + }), + ); + postMessageMock + .mockResolvedValueOnce({ ts: '123' }) + .mockResolvedValueOnce({}); + (SlackService as unknown as Mock).mockImplementation(() => ({ + postMessage: postMessageMock, + })); + + const result = await sendReportToSlack('target.json', 'finapp.json'); + + expect(result).toBe('Report sent to Slack'); + expect(readJsonFileMock).toHaveBeenCalledTimes(2); + expect(generateReportMock).toHaveBeenCalled(); + expect(postMessageMock).toHaveBeenCalledTimes(2); + expect(postMessageMock).toHaveBeenCalledWith('summary'); + expect(postMessageMock).toHaveBeenCalledWith('details', '123'); + }); + + it('throws AppError if readJsonFile fails', async () => { + readJsonFileMock.mockRejectedValueOnce(new Error('fail')); + await expect( + sendReportToSlack('target.json', 'finapp.json'), + ).rejects.toThrow(AppError); + await expect( + sendReportToSlack('target.json', 'finapp.json'), + ).rejects.toThrow('Failed to send report to Slack'); + }); + + it('throws AppError if generateReport fails', async () => { + readJsonFileMock + .mockResolvedValueOnce(mockTargetUnits) + .mockResolvedValueOnce(mockFinancialsAppData); + generateReportMock.mockRejectedValueOnce(new Error('fail-gen')); + (WeeklyFinancialReportRepository as unknown as Mock).mockImplementation( + () => ({ + generateReport: generateReportMock, + }), + ); + (SlackService as unknown as Mock).mockImplementation(() => ({ + postMessage: postMessageMock, + })); + await expect( + sendReportToSlack('target.json', 'finapp.json'), + ).rejects.toThrow(AppError); + }); + + it('throws AppError if postMessage fails', async () => { + readJsonFileMock + .mockResolvedValueOnce(mockTargetUnits) + .mockResolvedValueOnce(mockFinancialsAppData); + generateReportMock.mockReturnValue({ + details: 'details', + summary: 'summary', + }); + (WeeklyFinancialReportRepository as unknown as Mock).mockImplementation( + () => ({ + generateReport: generateReportMock, + }), + ); + postMessageMock.mockRejectedValueOnce(new Error('fail-post')); + (SlackService as unknown as Mock).mockImplementation(() => ({ + postMessage: postMessageMock, + })); + await expect( + sendReportToSlack('target.json', 'finapp.json'), + ).rejects.toThrow(AppError); + }); +}); diff --git a/workers/main/src/activities/weeklyFinancialReports/sendReportToSlack.ts b/workers/main/src/activities/weeklyFinancialReports/sendReportToSlack.ts new file mode 100644 index 0000000..e889186 --- /dev/null +++ b/workers/main/src/activities/weeklyFinancialReports/sendReportToSlack.ts @@ -0,0 +1,36 @@ +import { AppError } from '../../common/errors'; +import { readJsonFile } from '../../common/fileUtils'; +import { TargetUnit } from '../../common/types'; +import { FinancialsAppData } from '../../services/FinApp'; +import { SlackService } from '../../services/SlackService'; +import { WeeklyFinancialReportRepository } from '../../services/WeeklyFinancialReport'; + +export const sendReportToSlack = async ( + targetUnitsFileLink: string, + financialAppDataFileLink: string, +): Promise => { + try { + const [targetUnits, { employees, projects }] = await Promise.all([ + readJsonFile(targetUnitsFileLink), + readJsonFile(financialAppDataFileLink), + ]); + const weeklyFinancialReportRepository = + new WeeklyFinancialReportRepository(); + const { details, summary } = + await weeklyFinancialReportRepository.generateReport({ + targetUnits, + employees, + projects, + }); + const slackService = new SlackService(); + const message = await slackService.postMessage(summary); + + await slackService.postMessage(details, message.ts); + + return 'Report sent to Slack'; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + + throw new AppError('Failed to send report to Slack', message); + } +}; diff --git a/workers/main/src/services/FinApp/types.ts b/workers/main/src/services/FinApp/types.ts index 67021a1..b37ee10 100644 --- a/workers/main/src/services/FinApp/types.ts +++ b/workers/main/src/services/FinApp/types.ts @@ -23,3 +23,8 @@ export interface Project { history?: History; [key: string]: unknown; } + +export interface FinancialsAppData { + projects: Project[]; + employees: Employee[]; +}