Skip to content

Commit 0f557e5

Browse files
Merge branch 'main' into feature/add_deptech_hours
2 parents 6960293 + c1b5b38 commit 0f557e5

5 files changed

Lines changed: 185 additions & 5 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './fetchFinancialAppData';
22
export * from './getTargetUnits';
3+
export * from './sendReportToSlack';
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
2+
3+
import { AppError } from '../../common/errors';
4+
import { readJsonFile } from '../../common/fileUtils';
5+
import { TargetUnit } from '../../common/types';
6+
import { FinancialsAppData } from '../../services/FinApp';
7+
import { SlackService } from '../../services/SlackService';
8+
import { WeeklyFinancialReportRepository } from '../../services/WeeklyFinancialReport';
9+
import { sendReportToSlack } from './sendReportToSlack';
10+
11+
vi.mock('../../common/fileUtils', () => ({
12+
readJsonFile: vi.fn(),
13+
}));
14+
vi.mock('../../services/WeeklyFinancialReport', () => ({
15+
WeeklyFinancialReportRepository: vi.fn(),
16+
}));
17+
vi.mock('../../services/SlackService', () => ({
18+
SlackService: vi.fn(),
19+
}));
20+
21+
const mockTargetUnits: TargetUnit[] = [
22+
{
23+
group_id: 1,
24+
group_name: 'Group',
25+
project_id: 2,
26+
project_name: 'Project',
27+
user_id: 3,
28+
username: 'User',
29+
spent_on: '2024-06-01',
30+
total_hours: 8,
31+
},
32+
];
33+
const mockFinancialsAppData: FinancialsAppData = {
34+
employees: [{ redmine_id: 3, history: { rate: { '2024-06-01': 100 } } }],
35+
projects: [{ redmine_id: 2, history: { rate: { '2024-06-01': 200 } } }],
36+
};
37+
38+
describe('sendReportToSlack', () => {
39+
let readJsonFileMock: Mock;
40+
let generateReportMock: Mock;
41+
let postMessageMock: Mock;
42+
43+
function tryMockReset(obj: unknown) {
44+
if (
45+
typeof obj === 'function' &&
46+
'mockReset' in obj &&
47+
typeof (obj as { mockReset: unknown }).mockReset === 'function'
48+
) {
49+
(obj as { mockReset: () => void }).mockReset();
50+
}
51+
}
52+
53+
beforeEach(() => {
54+
readJsonFileMock = vi.mocked(readJsonFile);
55+
generateReportMock = vi.fn();
56+
postMessageMock = vi.fn();
57+
58+
tryMockReset(WeeklyFinancialReportRepository);
59+
tryMockReset(SlackService);
60+
});
61+
62+
it('sends report to Slack and returns success message', async () => {
63+
readJsonFileMock
64+
.mockResolvedValueOnce(mockTargetUnits)
65+
.mockResolvedValueOnce(mockFinancialsAppData);
66+
generateReportMock.mockReturnValue({
67+
details: 'details',
68+
summary: 'summary',
69+
});
70+
(WeeklyFinancialReportRepository as unknown as Mock).mockImplementation(
71+
() => ({
72+
generateReport: generateReportMock,
73+
}),
74+
);
75+
postMessageMock
76+
.mockResolvedValueOnce({ ts: '123' })
77+
.mockResolvedValueOnce({});
78+
(SlackService as unknown as Mock).mockImplementation(() => ({
79+
postMessage: postMessageMock,
80+
}));
81+
82+
const result = await sendReportToSlack('target.json', 'finapp.json');
83+
84+
expect(result).toBe('Report sent to Slack');
85+
expect(readJsonFileMock).toHaveBeenCalledTimes(2);
86+
expect(generateReportMock).toHaveBeenCalled();
87+
expect(postMessageMock).toHaveBeenCalledTimes(2);
88+
expect(postMessageMock).toHaveBeenCalledWith('summary');
89+
expect(postMessageMock).toHaveBeenCalledWith('details', '123');
90+
});
91+
92+
it('throws AppError if readJsonFile fails', async () => {
93+
readJsonFileMock.mockRejectedValueOnce(new Error('fail'));
94+
await expect(
95+
sendReportToSlack('target.json', 'finapp.json'),
96+
).rejects.toThrow(AppError);
97+
await expect(
98+
sendReportToSlack('target.json', 'finapp.json'),
99+
).rejects.toThrow('Failed to send report to Slack');
100+
});
101+
102+
it('throws AppError if generateReport fails', async () => {
103+
readJsonFileMock
104+
.mockResolvedValueOnce(mockTargetUnits)
105+
.mockResolvedValueOnce(mockFinancialsAppData);
106+
generateReportMock.mockRejectedValueOnce(new Error('fail-gen'));
107+
(WeeklyFinancialReportRepository as unknown as Mock).mockImplementation(
108+
() => ({
109+
generateReport: generateReportMock,
110+
}),
111+
);
112+
(SlackService as unknown as Mock).mockImplementation(() => ({
113+
postMessage: postMessageMock,
114+
}));
115+
await expect(
116+
sendReportToSlack('target.json', 'finapp.json'),
117+
).rejects.toThrow(AppError);
118+
});
119+
120+
it('throws AppError if postMessage fails', async () => {
121+
readJsonFileMock
122+
.mockResolvedValueOnce(mockTargetUnits)
123+
.mockResolvedValueOnce(mockFinancialsAppData);
124+
generateReportMock.mockReturnValue({
125+
details: 'details',
126+
summary: 'summary',
127+
});
128+
(WeeklyFinancialReportRepository as unknown as Mock).mockImplementation(
129+
() => ({
130+
generateReport: generateReportMock,
131+
}),
132+
);
133+
postMessageMock.mockRejectedValueOnce(new Error('fail-post'));
134+
(SlackService as unknown as Mock).mockImplementation(() => ({
135+
postMessage: postMessageMock,
136+
}));
137+
await expect(
138+
sendReportToSlack('target.json', 'finapp.json'),
139+
).rejects.toThrow(AppError);
140+
});
141+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { AppError } from '../../common/errors';
2+
import { readJsonFile } from '../../common/fileUtils';
3+
import { TargetUnit } from '../../common/types';
4+
import { FinancialsAppData } from '../../services/FinApp';
5+
import { SlackService } from '../../services/SlackService';
6+
import { WeeklyFinancialReportRepository } from '../../services/WeeklyFinancialReport';
7+
8+
export const sendReportToSlack = async (
9+
targetUnitsFileLink: string,
10+
financialAppDataFileLink: string,
11+
): Promise<string> => {
12+
try {
13+
const [targetUnits, { employees, projects }] = await Promise.all([
14+
readJsonFile<TargetUnit[]>(targetUnitsFileLink),
15+
readJsonFile<FinancialsAppData>(financialAppDataFileLink),
16+
]);
17+
const weeklyFinancialReportRepository =
18+
new WeeklyFinancialReportRepository();
19+
const { details, summary } =
20+
await weeklyFinancialReportRepository.generateReport({
21+
targetUnits,
22+
employees,
23+
projects,
24+
});
25+
const slackService = new SlackService();
26+
const message = await slackService.postMessage(summary);
27+
28+
await slackService.postMessage(details, message.ts);
29+
30+
return 'Report sent to Slack';
31+
} catch (err) {
32+
const message = err instanceof Error ? err.message : String(err);
33+
34+
throw new AppError('Failed to send report to Slack', message);
35+
}
36+
};

workers/main/src/services/FinApp/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ export interface Project {
2323
history?: History;
2424
[key: string]: unknown;
2525
}
26+
27+
export interface FinancialsAppData {
28+
projects: Project[];
29+
employees: Employee[];
30+
}

workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportRepository.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,8 @@ export class WeeklyFinancialReportRepository
151151
}
152152

153153
private composeWeeklyReportTitle(currentDate: Date): string {
154-
const periodStart = new Date(
155-
currentDate.getFullYear(),
156-
currentDate.getMonth(),
157-
currentDate.getDate() - ((currentDate.getDay() + 6) % 7) - 7,
158-
)
154+
const quarter = Math.floor(currentDate.getMonth() / 3);
155+
const periodStart = new Date(currentDate.getFullYear(), quarter * 3, 1)
159156
.toISOString()
160157
.slice(0, 10);
161158
const periodEnd = new Date(

0 commit comments

Comments
 (0)