Skip to content

Commit 15e87c7

Browse files
Merge branch 'main' into feature/process-group-name-parameter
2 parents d6ad0c8 + 52a2dd7 commit 15e87c7

File tree

4 files changed

+245
-2
lines changed

4 files changed

+245
-2
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import type { Connection } from 'mongoose';
2+
import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
3+
4+
import { AppError } from '../../common/errors';
5+
import * as fileUtils from '../../common/fileUtils';
6+
import * as mongoPoolModule from '../../common/MongoPool';
7+
import type { TargetUnit } from '../../common/types';
8+
import type { Employee, Project } from '../../services/FinApp';
9+
import type { IFinAppRepository } from '../../services/FinApp';
10+
import * as finAppService from '../../services/FinApp';
11+
import { fetchFinancialAppData } from './fetchFinancialAppData';
12+
13+
type MongoPoolMock = {
14+
connect: () => Promise<Connection>;
15+
disconnect: () => Promise<void>;
16+
getConnection: () => unknown;
17+
connection: null;
18+
uri: string;
19+
};
20+
21+
vi.mock('../../common/fileUtils', () => ({
22+
readJsonFile: vi.fn(),
23+
writeJsonFile: vi.fn(),
24+
}));
25+
vi.mock('../../common/MongoPool', () => ({
26+
MongoPool: {
27+
getInstance: vi.fn(),
28+
},
29+
}));
30+
vi.mock('../../services/FinApp', () => ({
31+
FinAppRepository: vi.fn(),
32+
}));
33+
34+
const mockTargetUnits: TargetUnit[] = [
35+
{
36+
group_id: 1,
37+
group_name: 'Group',
38+
project_id: 2,
39+
project_name: 'Project',
40+
user_id: 3,
41+
username: 'User',
42+
spent_on: '2024-06-01',
43+
total_hours: 8,
44+
},
45+
];
46+
const mockEmployees: Employee[] = [
47+
{ redmine_id: 3, history: { rate: { '2024-01-01': 100 } } },
48+
];
49+
const mockProjects: Project[] = [
50+
{
51+
redmine_id: 2,
52+
quick_books_id: 10,
53+
history: { rate: { '2024-01-01': 200 } },
54+
},
55+
];
56+
57+
function createRepoInstance(
58+
overrides: Partial<IFinAppRepository> = {},
59+
): IFinAppRepository {
60+
return {
61+
getEmployeesByRedmineIds: vi.fn().mockResolvedValue(mockEmployees),
62+
getProjectsByRedmineIds: vi.fn().mockResolvedValue(mockProjects),
63+
...overrides,
64+
};
65+
}
66+
67+
function createMongoPoolInstance(
68+
connectImpl?: () => Promise<Connection>,
69+
disconnectImpl?: () => Promise<void>,
70+
): MongoPoolMock {
71+
return {
72+
connect: connectImpl || vi.fn().mockResolvedValue({} as Connection),
73+
disconnect: disconnectImpl || vi.fn().mockResolvedValue(undefined),
74+
getConnection: vi.fn(),
75+
connection: null,
76+
uri: '',
77+
};
78+
}
79+
80+
describe('getFinAppData', () => {
81+
let readJsonFile: Mock;
82+
let writeJsonFile: Mock;
83+
let connect: Mock;
84+
let disconnect: Mock;
85+
let FinAppRepository: Mock;
86+
let dateSpy: ReturnType<typeof vi.spyOn>;
87+
let repoInstance: IFinAppRepository;
88+
let mongoPoolInstance: MongoPoolMock;
89+
90+
const fileLink = 'input.json';
91+
const expectedFilename =
92+
'data/weeklyFinancialReportsWorkflow/getFinAppData/data-123.json';
93+
94+
function setupSuccessMocks() {
95+
readJsonFile.mockResolvedValue(mockTargetUnits);
96+
writeJsonFile.mockResolvedValue(undefined);
97+
(repoInstance.getEmployeesByRedmineIds as Mock).mockResolvedValue(
98+
mockEmployees,
99+
);
100+
(repoInstance.getProjectsByRedmineIds as Mock).mockResolvedValue(
101+
mockProjects,
102+
);
103+
}
104+
105+
async function expectAppError(promise: Promise<unknown>, msg: string) {
106+
await expect(promise).rejects.toThrow(AppError);
107+
await expect(promise).rejects.toThrow(msg);
108+
expect(() => mongoPoolInstance.disconnect()).not.toThrow();
109+
}
110+
111+
beforeEach(() => {
112+
dateSpy = vi.spyOn(Date, 'now').mockReturnValue(123);
113+
readJsonFile = vi.mocked(fileUtils.readJsonFile);
114+
writeJsonFile = vi.mocked(fileUtils.writeJsonFile);
115+
FinAppRepository = vi.mocked(finAppService.FinAppRepository);
116+
117+
repoInstance = createRepoInstance();
118+
FinAppRepository.mockImplementation(() => repoInstance);
119+
120+
connect = vi.fn().mockResolvedValue(undefined);
121+
disconnect = vi.fn().mockResolvedValue(undefined);
122+
mongoPoolInstance = createMongoPoolInstance(connect, disconnect);
123+
(mongoPoolModule.MongoPool.getInstance as Mock).mockReturnValue(
124+
mongoPoolInstance as unknown as mongoPoolModule.MongoPool,
125+
);
126+
});
127+
128+
afterEach(() => {
129+
dateSpy.mockRestore();
130+
vi.clearAllMocks();
131+
});
132+
133+
describe('success cases', () => {
134+
it('returns fileLink when successful', async () => {
135+
setupSuccessMocks();
136+
const result = await fetchFinancialAppData(fileLink);
137+
138+
expect(result).toEqual({ fileLink: expectedFilename });
139+
expect(() => mongoPoolInstance.connect()).not.toThrow();
140+
expect(() => mongoPoolInstance.disconnect()).not.toThrow();
141+
expect(readJsonFile).toHaveBeenCalledWith(fileLink);
142+
expect(writeJsonFile).toHaveBeenCalledWith(expectedFilename, {
143+
employees: mockEmployees,
144+
projects: mockProjects,
145+
});
146+
});
147+
148+
it('always disconnects the mongo pool', async () => {
149+
setupSuccessMocks();
150+
await fetchFinancialAppData(fileLink).catch(() => {});
151+
expect(() => mongoPoolInstance.disconnect()).not.toThrow();
152+
});
153+
});
154+
155+
describe('error cases', () => {
156+
it('throws AppError when readJsonFile throws', async () => {
157+
readJsonFile.mockRejectedValue(new Error('fail-read'));
158+
await expectAppError(
159+
fetchFinancialAppData(fileLink),
160+
'Failed to get Fin App Data',
161+
);
162+
});
163+
164+
it('throws AppError when getEmployees throws', async () => {
165+
readJsonFile.mockResolvedValue(mockTargetUnits);
166+
(repoInstance.getEmployeesByRedmineIds as Mock).mockRejectedValue(
167+
new Error('fail-employees'),
168+
);
169+
await expectAppError(
170+
fetchFinancialAppData(fileLink),
171+
'Failed to get Fin App Data',
172+
);
173+
});
174+
175+
it('throws AppError when getProjects throws', async () => {
176+
readJsonFile.mockResolvedValue(mockTargetUnits);
177+
(repoInstance.getProjectsByRedmineIds as Mock).mockRejectedValue(
178+
new Error('fail-projects'),
179+
);
180+
await expectAppError(
181+
fetchFinancialAppData(fileLink),
182+
'Failed to get Fin App Data',
183+
);
184+
});
185+
186+
it('throws AppError when writeJsonFile throws', async () => {
187+
readJsonFile.mockResolvedValue(mockTargetUnits);
188+
writeJsonFile.mockRejectedValue(new Error('fail-write'));
189+
await expectAppError(
190+
fetchFinancialAppData(fileLink),
191+
'Failed to get Fin App Data',
192+
);
193+
});
194+
});
195+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { AppError } from '../../common/errors';
2+
import { readJsonFile, writeJsonFile } from '../../common/fileUtils';
3+
import { MongoPool } from '../../common/MongoPool';
4+
import { TargetUnit } from '../../common/types';
5+
import { FinAppRepository } from '../../services/FinApp';
6+
7+
interface GetTargetUnitsResult {
8+
fileLink: string;
9+
}
10+
11+
const getUniqueIds = <T, K extends keyof T>(items: T[], key: K): T[K][] => [
12+
...new Set(items.map((item) => item[key])),
13+
];
14+
15+
export const fetchFinancialAppData = async (
16+
fileLink: string,
17+
): Promise<GetTargetUnitsResult> => {
18+
const mongoPool = MongoPool.getInstance();
19+
const filename = `data/weeklyFinancialReportsWorkflow/getFinAppData/data-${Date.now()}.json`;
20+
21+
try {
22+
await mongoPool.connect();
23+
const repo = new FinAppRepository();
24+
25+
const targetUnits = await readJsonFile<TargetUnit[]>(fileLink);
26+
const employeeIds = getUniqueIds(targetUnits, 'user_id');
27+
const projectIds = getUniqueIds(targetUnits, 'project_id');
28+
29+
const [employees, projects] = await Promise.all([
30+
repo.getEmployeesByRedmineIds(employeeIds),
31+
repo.getProjectsByRedmineIds(projectIds),
32+
]);
33+
34+
await writeJsonFile(filename, { employees, projects });
35+
36+
return { fileLink: filename };
37+
} catch (err) {
38+
const message = err instanceof Error ? err.message : String(err);
39+
40+
throw new AppError('Failed to get Fin App Data', message);
41+
} finally {
42+
await mongoPool.disconnect();
43+
}
44+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from './fetchFinancialAppData';
12
export * from './getTargetUnits';

workers/main/src/workflows/weeklyFinancialReports/weeklyFinancialReports.workflow.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { AppError } from '../../common/errors';
55
import { GroupName } from '../../common/types';
66
import { GroupNameEnum } from '../../configs/weeklyFinancialReport';
77

8-
const { getTargetUnits } = proxyActivities<typeof activities>({
8+
const { getTargetUnits, fetchFinancialAppData } = proxyActivities<
9+
typeof activities
10+
>({
911
startToCloseTimeout: '10 minutes',
1012
});
1113

@@ -19,6 +21,7 @@ export async function weeklyFinancialReportsWorkflow(
1921
);
2022
}
2123
const targetUnits = await getTargetUnits(groupName);
24+
const finData = await fetchFinancialAppData(targetUnits.fileLink);
2225

23-
return targetUnits.fileLink;
26+
return finData.fileLink;
2427
}

0 commit comments

Comments
 (0)