Skip to content

Commit 66653e3

Browse files
Enhance Weekly Financial Report functionality and sorting (#95)
- Integrated `QBORepository` to fetch effective revenue data, enhancing financial report accuracy. - Updated `WeeklyFinancialReportRepository` to include effective revenue calculations and sorting by marginality levels. - Modified report formatting to display effective revenue, margin, and marginality metrics. - Added tests to ensure correct sorting of groups by marginality and effective marginality. These changes improve the financial reporting capabilities, providing clearer insights into revenue and performance metrics. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Reports now include top-level and per-project Effective Revenue, plus Effective Margin and Effective Marginality lines. Group headers use a bold name, summaries split into High/Medium/Low and ordered by marginality (alphabetical within a level). Footer shows a dynamic effective-revenue date window; default window increased to 4 months. * **Tests** * Added sorting tests and a test-data builder to validate marginality ordering and report formatting. * **Chores** * Project records now include a required name field; QBO revenue integration wired into data fetches and tests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent e29aa60 commit 66653e3

13 files changed

+603
-142
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Enhancement Archive: Add Effective Financial Parameters to Target Units
2+
3+
## Summary
4+
Implemented integration of effective financial parameters (Effective Revenue, Effective Margin, Effective Marginality) into the Weekly Financial Reports system through QuickBooks Online (QBO) integration.
5+
6+
## Date Completed
7+
2025-08-11
8+
9+
## Key Files Modified
10+
- `workers/main/src/activities/weeklyFinancialReports/fetchFinancialAppData.ts` - QBO integration for effective revenue fetching
11+
- `workers/main/src/configs/qbo.ts` - QBO configuration with effective revenue parameters
12+
- `workers/main/src/services/FinApp/types.ts` - added effectiveRevenue field to Project interface
13+
- `workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportFormatter.ts` - formatting new metrics in reports
14+
- `workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportRepository.ts` - core business logic for calculations
15+
- `workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportRepository.test.ts` - extended testing
16+
- `workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportSorting.test.ts` - new sorting tests
17+
18+
## Requirements Addressed
19+
- Add Effective Revenue as Target Unit parameter
20+
- Add Effective Margin as Target Unit parameter
21+
- Add Effective Marginality as Target Unit parameter
22+
- Integrate data from QuickBooks Online for accurate calculations
23+
- Implement group sorting by marginality levels (High → Medium → Low)
24+
- Ensure display of new metrics in financial reports
25+
26+
## Implementation Details
27+
**QBO Integration:**
28+
- Added `qboRepo.getEffectiveRevenue()` call in `fetchFinancialAppData`
29+
- Configured with `effectiveRevenueMonths` parameter (default 4 months)
30+
- Effective revenue linked to projects through `quick_books_id`
31+
32+
**Calculations and Sorting:**
33+
- Implemented effectiveRevenue, effectiveMargin, effectiveMarginality calculations in `aggregateGroupData`
34+
- Added advanced sorting: first by marginality levels, then by effective marginality
35+
- `compareMarginalityLevels` method for sorting order determination (High: 3, Medium: 2, Low: 1)
36+
37+
**Formatting:**
38+
- Updated `formatDetail` to display Effective Revenue, Effective Margin, Effective Marginality
39+
- Added explanations in footer about effective revenue calculation period
40+
- Support for marginality indicators in reports
41+
42+
## Testing Performed
43+
- Unit tests for new `aggregateGroupData` method with effective metrics calculations
44+
- Comprehensive sorting tests in `WeeklyFinancialReportSorting.test.ts` (100+ lines)
45+
- Verification of correct sorting by marginality levels: High → Medium → Low
46+
- Testing secondary sorting by effective marginality within same level
47+
- Validation of new field formatting in reports
48+
49+
## Lessons Learned
50+
- **QBO integration**: Repository pattern is effective for external services and scales well
51+
- **Financial calculations**: Require particularly detailed testing due to critical importance of accuracy
52+
- **Level 2 complexity**: Tasks with external service integration can be more complex than expected (+401 lines for Level 2)
53+
- **Code organization**: Proper separation of responsibilities between data, business, and presentation layers is critical
54+
- **Optimization**: The `compareMarginalityLevels` method can be inlined for simplification (~15 lines savings)
55+
56+
## Related Work
57+
- Related to general Weekly Financial Reports system
58+
- Based on existing QBORepository infrastructure
59+
- Complements marginality system (MarginalityCalculator, MarginalityLevel)
60+
- PR #95: https://github.com/speedandfunction/automatization/pull/95
61+
62+
## Notes
63+
**Technical Architecture:**
64+
- Used Repository pattern for QBO integration
65+
- Preserved backward compatibility when adding new fields
66+
- Efficient design: minimal changes in types, focused changes in business logic
67+
68+
**Potential Improvements:**
69+
- Inline `compareMarginalityLevels` method
70+
- Extract marginality thresholds to configuration constants
71+
- More strict typing for financial calculations
72+
73+
**Time Estimates:**
74+
- Planned: 1-2 days (Level 2)
75+
- Actual: 2-3 days
76+
- Variance reason: underestimation of QBO integration complexity and testing volume required

workers/main/src/activities/weeklyFinancialReports/fetchFinancialAppData.test.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ import { AppError } from '../../common/errors';
55
import * as fileUtils from '../../common/fileUtils';
66
import * as mongoPoolModule from '../../common/MongoPool';
77
import type { TargetUnit } from '../../common/types';
8-
import type { Employee, Project } from '../../services/FinApp';
9-
import type { IFinAppRepository } from '../../services/FinApp';
8+
import type {
9+
Employee,
10+
IFinAppRepository,
11+
Project,
12+
} from '../../services/FinApp';
1013
import * as finAppService from '../../services/FinApp';
14+
import type { CustomerRevenueByRef } from '../../services/QBO';
15+
import * as qboService from '../../services/QBO';
1116
import { fetchFinancialAppData } from './fetchFinancialAppData';
1217

1318
type MongoPoolMock = {
@@ -30,6 +35,9 @@ vi.mock('../../common/MongoPool', () => ({
3035
vi.mock('../../services/FinApp', () => ({
3136
FinAppRepository: vi.fn(),
3237
}));
38+
vi.mock('../../services/QBO', () => ({
39+
QBORepository: vi.fn(),
40+
}));
3341

3442
const mockTargetUnits: TargetUnit[] = [
3543
{
@@ -48,12 +56,21 @@ const mockEmployees: Employee[] = [
4856
];
4957
const mockProjects: Project[] = [
5058
{
59+
name: 'Test Project',
5160
redmine_id: 2,
5261
quick_books_id: 10,
5362
history: { rate: { '2024-01-01': 200 } },
5463
},
5564
];
5665

66+
const mockEffectiveRevenue: CustomerRevenueByRef = {
67+
'10': {
68+
customerName: 'Test Customer',
69+
totalAmount: 5000,
70+
invoiceCount: 3,
71+
},
72+
};
73+
5774
function createRepoInstance(
5875
overrides: Partial<IFinAppRepository> = {},
5976
): IFinAppRepository {
@@ -83,8 +100,10 @@ describe('getFinAppData', () => {
83100
let connect: Mock;
84101
let disconnect: Mock;
85102
let FinAppRepository: Mock;
103+
let qboRepository: Mock;
86104
let dateSpy: ReturnType<typeof vi.spyOn>;
87105
let repoInstance: IFinAppRepository;
106+
let qboRepoInstance: { getEffectiveRevenue: Mock };
88107
let mongoPoolInstance: MongoPoolMock;
89108

90109
const fileLink = 'input.json';
@@ -100,6 +119,7 @@ describe('getFinAppData', () => {
100119
(repoInstance.getProjectsByRedmineIds as Mock).mockResolvedValue(
101120
mockProjects,
102121
);
122+
qboRepoInstance.getEffectiveRevenue.mockResolvedValue(mockEffectiveRevenue);
103123
}
104124

105125
async function expectAppError(promise: Promise<unknown>, msg: string) {
@@ -113,10 +133,16 @@ describe('getFinAppData', () => {
113133
readJsonFile = vi.mocked(fileUtils.readJsonFile);
114134
writeJsonFile = vi.mocked(fileUtils.writeJsonFile);
115135
FinAppRepository = vi.mocked(finAppService.FinAppRepository);
136+
qboRepository = vi.mocked(qboService.QBORepository);
116137

117138
repoInstance = createRepoInstance();
118139
FinAppRepository.mockImplementation(() => repoInstance);
119140

141+
qboRepoInstance = {
142+
getEffectiveRevenue: vi.fn().mockResolvedValue(mockEffectiveRevenue),
143+
};
144+
qboRepository.mockImplementation(() => qboRepoInstance);
145+
120146
connect = vi.fn().mockResolvedValue(undefined);
121147
disconnect = vi.fn().mockResolvedValue(undefined);
122148
mongoPoolInstance = createMongoPoolInstance(connect, disconnect);
@@ -141,8 +167,16 @@ describe('getFinAppData', () => {
141167
expect(readJsonFile).toHaveBeenCalledWith(fileLink);
142168
expect(writeJsonFile).toHaveBeenCalledWith(expectedFilename, {
143169
employees: mockEmployees,
144-
projects: mockProjects,
170+
projects: [
171+
{
172+
...mockProjects[0],
173+
effectiveRevenue: 5000,
174+
},
175+
],
176+
effectiveRevenue: mockEffectiveRevenue,
145177
});
178+
expect(qboRepository).toHaveBeenCalledTimes(1);
179+
expect(qboRepoInstance.getEffectiveRevenue).toHaveBeenCalledTimes(1);
146180
});
147181

148182
it('always disconnects the mongo pool', async () => {

workers/main/src/activities/weeklyFinancialReports/fetchFinancialAppData.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { readJsonFile, writeJsonFile } from '../../common/fileUtils';
33
import { MongoPool } from '../../common/MongoPool';
44
import { TargetUnit } from '../../common/types';
55
import { FinAppRepository } from '../../services/FinApp';
6+
import { QBORepository } from '../../services/QBO';
67

78
interface GetTargetUnitsResult {
89
fileLink: string;
@@ -21,17 +22,30 @@ export const fetchFinancialAppData = async (
2122
try {
2223
await mongoPool.connect();
2324
const repo = new FinAppRepository();
25+
const qboRepo = new QBORepository();
2426

2527
const targetUnits = await readJsonFile<TargetUnit[]>(fileLink);
2628
const employeeIds = getUniqueIds(targetUnits, 'user_id');
2729
const projectIds = getUniqueIds(targetUnits, 'project_id');
2830

29-
const [employees, projects] = await Promise.all([
30-
repo.getEmployeesByRedmineIds(employeeIds),
31-
repo.getProjectsByRedmineIds(projectIds),
32-
]);
33-
34-
await writeJsonFile(filename, { employees, projects });
31+
const [employees, projects, effectiveRevenueByCustomerRef] =
32+
await Promise.all([
33+
repo.getEmployeesByRedmineIds(employeeIds),
34+
repo.getProjectsByRedmineIds(projectIds),
35+
qboRepo.getEffectiveRevenue(),
36+
]);
37+
38+
await writeJsonFile(filename, {
39+
employees,
40+
projects: projects.map((project) => ({
41+
...project,
42+
effectiveRevenue: project.quick_books_id
43+
? effectiveRevenueByCustomerRef[String(project.quick_books_id)]
44+
?.totalAmount || 0
45+
: 0,
46+
})),
47+
effectiveRevenue: effectiveRevenueByCustomerRef,
48+
});
3549

3650
return { fileLink: filename };
3751
} catch (err) {

workers/main/src/configs/qbo.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ export const qboConfig = {
1111
tokenHost: 'https://oauth.platform.intuit.com',
1212
tokenPath: '/oauth2/v1/tokens/bearer',
1313
tokenExpirationWindowSeconds: 300,
14-
effectiveRevenueMonths: parseInt(
15-
process.env.QBO_EFFECTIVE_REVENUE_MONTHS || '3',
16-
),
14+
effectiveRevenueMonths: (() => {
15+
const raw = Number(process.env.QBO_EFFECTIVE_REVENUE_MONTHS);
16+
17+
return Number.isFinite(raw) ? Math.trunc(raw) : 4;
18+
})(),
1719
};
1820

1921
export const qboSchema = z.object({

workers/main/src/services/FinApp/FinAppRepository.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ describe('FinAppRepository', () => {
120120
expect(result).toEqual(mockProjects);
121121
expect(vi.mocked(ProjectModel).find).toHaveBeenCalledWith(
122122
{ redmine_id: { $in: [550] } },
123-
{ 'redmine_id': 1, 'quick_books_id': 1, 'history.rate': 1 },
123+
{ 'name': 1, 'redmine_id': 1, 'quick_books_id': 1, 'history.rate': 1 },
124124
);
125125
});
126126

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class FinAppRepository implements IFinAppRepository {
2121
try {
2222
return await ProjectModel.find(
2323
{ redmine_id: { $in: redmineIds } },
24-
{ 'redmine_id': 1, 'quick_books_id': 1, 'history.rate': 1 },
24+
{ 'name': 1, 'redmine_id': 1, 'quick_books_id': 1, 'history.rate': 1 },
2525
).lean<Project[]>();
2626
} catch (error) {
2727
throw new FinAppRepositoryError(

workers/main/src/services/FinApp/FinAppSchemas.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe('FinApp Schemas', () => {
5959

6060
it('should accept valid project', async () => {
6161
const doc = new ProjectModel({
62+
name: 'Test Project',
6263
redmine_id: 456,
6364
quick_books_id: 789,
6465
history: { rate: { '2024-01-01': 200 } },

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const EmployeeModel = mongoose.model<Employee & Document>(
2424

2525
// Project schema: represents a project with Redmine and QuickBooks IDs, and a history of rates
2626
export const projectSchema = new mongoose.Schema({
27+
name: { type: String, required: true },
2728
redmine_id: { type: Number, required: true, index: true },
2829
quick_books_id: Number,
2930
history: historySchema,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export interface Employee {
1212
}
1313

1414
export interface Project {
15+
/**
16+
* Project name
17+
*/
18+
name: string;
1519
/**
1620
* Redmine project ID (links to the corresponding project in Redmine)
1721
*/
@@ -21,6 +25,7 @@ export interface Project {
2125
*/
2226
quick_books_id?: number;
2327
history?: History;
28+
effectiveRevenue?: number;
2429
[key: string]: unknown;
2530
}
2631

0 commit comments

Comments
 (0)