Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ff7c757
Update ESLint configuration and dependencies
anatolyshipitz Aug 11, 2025
ef180af
Update ESLint configuration and dependencies
anatolyshipitz Aug 11, 2025
2094277
Merge branch 'main' into feature/eslint-naming-conventions
anatolyshipitz Aug 11, 2025
a2893db
Add comprehensive ESLint naming conventions and update configuration
anatolyshipitz Aug 11, 2025
c3a1c33
Enhance ESLint naming conventions and configuration
anatolyshipitz Aug 11, 2025
061120b
Merge branch 'main' into feature/eslint-naming-conventions
anatolyshipitz Aug 11, 2025
16a3ad1
Update ESLint naming conventions and configuration
anatolyshipitz Aug 11, 2025
6d8930f
Enhance Weekly Financial Report functionality and sorting
anatolyshipitz Aug 11, 2025
335c183
Add Effective Financial Parameters to Weekly Financial Reports
anatolyshipitz Aug 11, 2025
084e73c
Merge branch 'main' into feature/add_marginality_level
anatolyshipitz Aug 12, 2025
3a3d9be
Fix/weekly reports effective revenue calculation (#97)
anatolyshipitz Aug 13, 2025
fe3b6c9
Refactor WeeklyFinancialReportRepository to Preserve Group Order
anatolyshipitz Aug 13, 2025
cf413a0
Refactor effectiveRevenueMonths calculation in qboConfig
anatolyshipitz Aug 13, 2025
c57099e
Enhance date calculation logic in WeeklyFinancialReportFormatter
anatolyshipitz Aug 13, 2025
24c143d
Refactor fetchFinancialAppData to Integrate QBO Effective Revenue
anatolyshipitz Aug 13, 2025
b1daa10
Refactor Tests and Improve Effective Revenue Calculation in Weekly Fi…
anatolyshipitz Aug 13, 2025
000890b
Update FinAppRepository Tests and Project Interface
anatolyshipitz Aug 13, 2025
b768575
Refactor Weekly Financial Report Tests and Sorting Logic
anatolyshipitz Aug 13, 2025
8afbad0
Enhance Weekly Financial Report Tests and Repository Structure
anatolyshipitz Aug 13, 2025
afe4e22
Refactor QBORepository Mocking in Financial App Data Tests
anatolyshipitz Aug 13, 2025
3f5492f
Fix Type Handling in Financial App Data Fetching
anatolyshipitz Aug 13, 2025
b4c01c4
Enhance Weekly Financial Report Structure with Total Hours
anatolyshipitz Aug 13, 2025
410826e
Update FinApp and Weekly Financial Report Tests for Consistency
anatolyshipitz Aug 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Enhancement Archive: Add Effective Financial Parameters to Target Units

## Summary
Implemented integration of effective financial parameters (Effective Revenue, Effective Margin, Effective Marginality) into the Weekly Financial Reports system through QuickBooks Online (QBO) integration.

## Date Completed
2025-08-11

## Key Files Modified
- `workers/main/src/activities/weeklyFinancialReports/fetchFinancialAppData.ts` - QBO integration for effective revenue fetching
- `workers/main/src/configs/qbo.ts` - QBO configuration with effective revenue parameters
- `workers/main/src/services/FinApp/types.ts` - added effectiveRevenue field to Project interface
- `workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportFormatter.ts` - formatting new metrics in reports
- `workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportRepository.ts` - core business logic for calculations
- `workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportRepository.test.ts` - extended testing
- `workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportSorting.test.ts` - new sorting tests

## Requirements Addressed
- Add Effective Revenue as Target Unit parameter
- Add Effective Margin as Target Unit parameter
- Add Effective Marginality as Target Unit parameter
- Integrate data from QuickBooks Online for accurate calculations
- Implement group sorting by marginality levels (High → Medium → Low)
- Ensure display of new metrics in financial reports

## Implementation Details
**QBO Integration:**
- Added `qboRepo.getEffectiveRevenue()` call in `fetchFinancialAppData`
- Configured with `effectiveRevenueMonths` parameter (default 4 months)
- Effective revenue linked to projects through `quick_books_id`

**Calculations and Sorting:**
- Implemented effectiveRevenue, effectiveMargin, effectiveMarginality calculations in `aggregateGroupData`
- Added advanced sorting: first by marginality levels, then by effective marginality
- `compareMarginalityLevels` method for sorting order determination (High: 3, Medium: 2, Low: 1)

**Formatting:**
- Updated `formatDetail` to display Effective Revenue, Effective Margin, Effective Marginality
- Added explanations in footer about effective revenue calculation period
- Support for marginality indicators in reports

## Testing Performed
- Unit tests for new `aggregateGroupData` method with effective metrics calculations
- Comprehensive sorting tests in `WeeklyFinancialReportSorting.test.ts` (100+ lines)
- Verification of correct sorting by marginality levels: High → Medium → Low
- Testing secondary sorting by effective marginality within same level
- Validation of new field formatting in reports

## Lessons Learned
- **QBO integration**: Repository pattern is effective for external services and scales well
- **Financial calculations**: Require particularly detailed testing due to critical importance of accuracy
- **Level 2 complexity**: Tasks with external service integration can be more complex than expected (+401 lines for Level 2)
- **Code organization**: Proper separation of responsibilities between data, business, and presentation layers is critical
- **Optimization**: The `compareMarginalityLevels` method can be inlined for simplification (~15 lines savings)

## Related Work
- Related to general Weekly Financial Reports system
- Based on existing QBORepository infrastructure
- Complements marginality system (MarginalityCalculator, MarginalityLevel)
- PR #95: https://github.com/speedandfunction/automatization/pull/95

## Notes
**Technical Architecture:**
- Used Repository pattern for QBO integration
- Preserved backward compatibility when adding new fields
- Efficient design: minimal changes in types, focused changes in business logic

**Potential Improvements:**
- Inline `compareMarginalityLevels` method
- Extract marginality thresholds to configuration constants
- More strict typing for financial calculations

**Time Estimates:**
- Planned: 1-2 days (Level 2)
- Actual: 2-3 days
- Variance reason: underestimation of QBO integration complexity and testing volume required
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import { AppError } from '../../common/errors';
import * as fileUtils from '../../common/fileUtils';
import * as mongoPoolModule from '../../common/MongoPool';
import type { TargetUnit } from '../../common/types';
import type { Employee, Project } from '../../services/FinApp';
import type { IFinAppRepository } from '../../services/FinApp';
import type {
Employee,
IFinAppRepository,
Project,
} from '../../services/FinApp';
import * as finAppService from '../../services/FinApp';
import type { CustomerRevenueByRef } from '../../services/QBO';
import * as qboService from '../../services/QBO';
import { fetchFinancialAppData } from './fetchFinancialAppData';

type MongoPoolMock = {
Expand All @@ -30,6 +35,9 @@ vi.mock('../../common/MongoPool', () => ({
vi.mock('../../services/FinApp', () => ({
FinAppRepository: vi.fn(),
}));
vi.mock('../../services/QBO', () => ({
QBORepository: vi.fn(),
}));

const mockTargetUnits: TargetUnit[] = [
{
Expand All @@ -49,11 +57,19 @@ const mockEmployees: Employee[] = [
const mockProjects: Project[] = [
{
redmine_id: 2,
quick_books_id: 10,
quick_books_id: '10',
history: { rate: { '2024-01-01': 200 } },
},
];

const mockEffectiveRevenue: CustomerRevenueByRef = {
'10': {
customerName: 'Test Customer',
totalAmount: 5000,
invoiceCount: 3,
},
};

function createRepoInstance(
overrides: Partial<IFinAppRepository> = {},
): IFinAppRepository {
Expand Down Expand Up @@ -83,8 +99,10 @@ describe('getFinAppData', () => {
let connect: Mock;
let disconnect: Mock;
let FinAppRepository: Mock;
let QBORepository: Mock;
let dateSpy: ReturnType<typeof vi.spyOn>;
let repoInstance: IFinAppRepository;
let qboRepoInstance: { getEffectiveRevenue: Mock };
let mongoPoolInstance: MongoPoolMock;

const fileLink = 'input.json';
Expand All @@ -100,6 +118,7 @@ describe('getFinAppData', () => {
(repoInstance.getProjectsByRedmineIds as Mock).mockResolvedValue(
mockProjects,
);
qboRepoInstance.getEffectiveRevenue.mockResolvedValue(mockEffectiveRevenue);
}

async function expectAppError(promise: Promise<unknown>, msg: string) {
Expand All @@ -113,10 +132,16 @@ describe('getFinAppData', () => {
readJsonFile = vi.mocked(fileUtils.readJsonFile);
writeJsonFile = vi.mocked(fileUtils.writeJsonFile);
FinAppRepository = vi.mocked(finAppService.FinAppRepository);
QBORepository = vi.mocked(qboService.QBORepository);

repoInstance = createRepoInstance();
FinAppRepository.mockImplementation(() => repoInstance);

qboRepoInstance = {
getEffectiveRevenue: vi.fn().mockResolvedValue(mockEffectiveRevenue),
};
QBORepository.mockImplementation(() => qboRepoInstance);

connect = vi.fn().mockResolvedValue(undefined);
disconnect = vi.fn().mockResolvedValue(undefined);
mongoPoolInstance = createMongoPoolInstance(connect, disconnect);
Expand All @@ -141,7 +166,13 @@ describe('getFinAppData', () => {
expect(readJsonFile).toHaveBeenCalledWith(fileLink);
expect(writeJsonFile).toHaveBeenCalledWith(expectedFilename, {
employees: mockEmployees,
projects: mockProjects,
projects: [
{
...mockProjects[0],
effectiveRevenue: 5000,
},
],
effectiveRevenue: mockEffectiveRevenue,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readJsonFile, writeJsonFile } from '../../common/fileUtils';
import { MongoPool } from '../../common/MongoPool';
import { TargetUnit } from '../../common/types';
import { FinAppRepository } from '../../services/FinApp';
import { QBORepository } from '../../services/QBO';

interface GetTargetUnitsResult {
fileLink: string;
Expand All @@ -21,17 +22,30 @@ export const fetchFinancialAppData = async (
try {
await mongoPool.connect();
const repo = new FinAppRepository();
const qboRepo = new QBORepository();

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

const [employees, projects] = await Promise.all([
repo.getEmployeesByRedmineIds(employeeIds),
repo.getProjectsByRedmineIds(projectIds),
]);

await writeJsonFile(filename, { employees, projects });
const [employees, projects, effectiveRevenueByCustomerRef] =
await Promise.all([
repo.getEmployeesByRedmineIds(employeeIds),
repo.getProjectsByRedmineIds(projectIds),
qboRepo.getEffectiveRevenue(),
]);

await writeJsonFile(filename, {
employees,
projects: projects.map((project) => ({
...project,
effectiveRevenue: project.quick_books_id
? effectiveRevenueByCustomerRef[project.quick_books_id]
?.totalAmount || 0
: 0,
})),
effectiveRevenue: effectiveRevenueByCustomerRef,
});

return { fileLink: filename };
} catch (err) {
Expand Down
8 changes: 5 additions & 3 deletions workers/main/src/configs/qbo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ export const qboConfig = {
tokenHost: 'https://oauth.platform.intuit.com',
tokenPath: '/oauth2/v1/tokens/bearer',
tokenExpirationWindowSeconds: 300,
effectiveRevenueMonths: parseInt(
process.env.QBO_EFFECTIVE_REVENUE_MONTHS || '3',
),
effectiveRevenueMonths: (() => {
const raw = Number(process.env.QBO_EFFECTIVE_REVENUE_MONTHS);

return Number.isFinite(raw) ? Math.trunc(raw) : 4;
})(),
};

export const qboSchema = z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ describe('FinAppRepository', () => {
expect(result).toEqual(mockProjects);
expect(vi.mocked(ProjectModel).find).toHaveBeenCalledWith(
{ redmine_id: { $in: [550] } },
{ 'redmine_id': 1, 'quick_books_id': 1, 'history.rate': 1 },
{ 'name': 1, 'redmine_id': 1, 'quick_books_id': 1, 'history.rate': 1 },
);
});

Expand Down
2 changes: 1 addition & 1 deletion workers/main/src/services/FinApp/FinAppRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class FinAppRepository implements IFinAppRepository {
try {
return await ProjectModel.find(
{ redmine_id: { $in: redmineIds } },
{ 'redmine_id': 1, 'quick_books_id': 1, 'history.rate': 1 },
{ 'name': 1, 'redmine_id': 1, 'quick_books_id': 1, 'history.rate': 1 },
).lean<Project[]>();
} catch (error) {
throw new FinAppRepositoryError(
Expand Down
1 change: 1 addition & 0 deletions workers/main/src/services/FinApp/FinAppSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const EmployeeModel = mongoose.model<Employee & Document>(

// Project schema: represents a project with Redmine and QuickBooks IDs, and a history of rates
export const projectSchema = new mongoose.Schema({
name: { type: String, required: true },
redmine_id: { type: Number, required: true, index: true },
quick_books_id: Number,
history: historySchema,
Expand Down
5 changes: 5 additions & 0 deletions workers/main/src/services/FinApp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export interface Employee {
}

export interface Project {
/**
* Project name
*/
name: string;
/**
* Redmine project ID (links to the corresponding project in Redmine)
*/
Expand All @@ -21,6 +25,7 @@ export interface Project {
*/
quick_books_id?: number;
history?: History;
effectiveRevenue?: number;
[key: string]: unknown;
}

Expand Down
Loading
Loading