Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -42,13 +42,14 @@
? effectiveRevenue[project.quick_books_id]?.totalAmount || 0
: 0,
})),
effectiveRevenue,
});

return { fileLink: filename };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);

throw new AppError('Failed to get Fin App Data', message);

Check failure on line 52 in workers/main/src/activities/weeklyFinancialReports/fetchFinancialAppData.ts

View workflow job for this annotation

GitHub Actions / SonarQube

src/activities/weeklyFinancialReports/fetchFinancialAppData.test.ts > getFinAppData > success cases > returns fileLink when successful

QBORepository.getEffectiveRevenue failed: QBORepository.getPaidInvoices failed: Invalid access token format: Failed to get Fin App Data ❯ Module.fetchFinancialAppData src/activities/weeklyFinancialReports/fetchFinancialAppData.ts:52:11 ❯ src/activities/weeklyFinancialReports/fetchFinancialAppData.test.ts:136:22
} finally {
await mongoPool.disconnect();
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export interface FormatSummaryInput {

export interface FormatDetailInput {
groupName: string;
groupTotalHours: number;
currentQuarter: string;
groupTotalRevenue: number;
groupTotalCogs: number;
Expand All @@ -32,7 +31,6 @@ const spacer = ' '.repeat(4);
export class WeeklyFinancialReportFormatter {
static formatDetail = ({
groupName,
groupTotalHours,
currentQuarter,
groupTotalRevenue,
groupTotalCogs,
Expand All @@ -43,15 +41,15 @@ export class WeeklyFinancialReportFormatter {
effectiveMargin,
effectiveMarginality,
}: FormatDetailInput) =>
`*${groupName}* (${groupTotalHours}h)\n` +
`${spacer}*Period*: ${currentQuarter}\n` +
`${spacer}*Revenue*: ${formatCurrency(groupTotalRevenue)}\n` +
`${spacer}*COGS*: ${formatCurrency(groupTotalCogs)}\n` +
`${spacer}*Margin*: ${formatCurrency(marginAmount)}\n` +
`${spacer}*Marginality*: ${marginalityPercent.toFixed(0)}%\n` +
`${spacer}*Effective Revenue*: ${formatCurrency(effectiveRevenue)}\n` +
`${spacer}*Effective Margin*: ${formatCurrency(effectiveMargin)}\n` +
`${spacer}*Effective Marginality*: ${indicator} ${effectiveMarginality.toFixed(0)}%\n\n`;
`*${groupName}*\n` +
`${spacer}period: ${currentQuarter}\n` +
`${spacer}revenue: ${formatCurrency(groupTotalRevenue)}\n` +
`${spacer}COGS: ${formatCurrency(groupTotalCogs)}\n` +
`${spacer}margin: ${formatCurrency(marginAmount)}\n` +
`${spacer}marginality: ${marginalityPercent.toFixed(0)}%\n` +
`${spacer}effective revenue: ${formatCurrency(effectiveRevenue)}\n` +
`${spacer}effective margin: ${formatCurrency(effectiveMargin)}\n` +
`${spacer}effective marginality: ${indicator} ${effectiveMarginality.toFixed(0)}%\n\n\n`;

static formatSummary = ({
reportTitle,
Expand All @@ -62,24 +60,24 @@ export class WeeklyFinancialReportFormatter {
let summary = `${reportTitle}\n`;

if (highGroups.length) {
summary += '________________________________\n';
summary += '\n_______________________\n\n\n';
summary += `:arrowup: *Marginality is ${HIGH_MARGINALITY_THRESHOLD}% or higher*:\n`;
summary += `${spacer}${highGroups.join(`\n${spacer}`)}\n`;
summary += `${spacer}${spacer}${highGroups.join(`\n${spacer}${spacer}`)}\n`;
}

if (mediumGroups.length) {
summary += '__________________________________\n';
summary += '\n_______________________\n\n\n';
summary += ` :large_yellow_circle: *Marginality is between ${MEDIUM_MARGINALITY_THRESHOLD}-${HIGH_MARGINALITY_THRESHOLD}%*:\n`;
summary += `${spacer}${mediumGroups.join(`\n${spacer}`)}\n`;
summary += `${spacer}${spacer}${mediumGroups.join(`\n${spacer}${spacer}`)}\n`;
}

if (lowGroups.length) {
summary += '__________________________________\n';
summary += '\n_______________________\n\n\n';
summary += `:arrowdown: *Marginality is under ${MEDIUM_MARGINALITY_THRESHOLD}%*:\n`;
summary += `${spacer}${lowGroups.join(`\n${spacer}`)}\n`;
summary += `${spacer}${spacer}${lowGroups.join(`\n${spacer}${spacer}`)}\n`;
}

summary += ' -------------------------------------------\n';
summary += '\n_______________________\n\n\n';
summary += 'The specific figures will be available in the thread';

return summary;
Expand All @@ -97,12 +95,11 @@ export class WeeklyFinancialReportFormatter {
};
}

static formatFooter = (totalHours: number) => {
static formatFooter = () => {
const { startDate, endDate } = this.calculateDateWindow();

return (
`\n*Total hours*: ${totalHours}h\n\n` +
'*Notes:*\n' +
'\n*Notes:*\n' +
'1. *Contract Type* is not implemented\n' +
`2. *Effective Revenue* calculated for the last ${qboConfig.effectiveRevenueMonths} months (${startDate} - ${endDate})\n` +
'3. *Dept Tech* hours are not implemented\n\n' +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { WeeklyFinancialReportFormatter } from './WeeklyFinancialReportFormatter

interface GroupData {
groupName: string;
groupTotalHours: number;
groupTotalRevenue: number;
groupTotalCogs: number;
effectiveRevenue: number;
Expand All @@ -26,8 +25,7 @@ interface GroupData {
}

export class WeeklyFinancialReportRepository
implements IWeeklyFinancialReportRepository
{
implements IWeeklyFinancialReportRepository {
async generateReport({
targetUnits,
employees,
Expand All @@ -45,12 +43,13 @@ export class WeeklyFinancialReportRepository

this.sortGroupData(groupData);

const { reportDetails: initialDetails, totalReportedHours } =
this.formatGroupDetails(groupData, currentQuarter);
const { reportDetails: initialDetails } = this.formatGroupDetails(
groupData,
currentQuarter,
);

const reportDetails =
initialDetails +
WeeklyFinancialReportFormatter.formatFooter(totalReportedHours);
initialDetails + WeeklyFinancialReportFormatter.formatFooter();

const { highGroups, mediumGroups, lowGroups } =
this.createSortedGroups(groupData);
Expand Down Expand Up @@ -101,7 +100,7 @@ export class WeeklyFinancialReportRepository
};

// Sort by marginality level (High -> Medium -> Low),
// then within each level by descending effectiveMarginality
// then within each level by groupName alphabetically
groupData.sort((a, b) => {
const levelComparison =
levelOrder[b.marginality.level] - levelOrder[a.marginality.level];
Expand All @@ -110,7 +109,8 @@ export class WeeklyFinancialReportRepository
return levelComparison;
}

return b.effectiveMarginality - a.effectiveMarginality;
// Sort by groupName alphabetically within each level
return a.groupName.localeCompare(b.groupName);
});
}

Expand All @@ -120,7 +120,7 @@ export class WeeklyFinancialReportRepository
employees: Employee[],
projects: Project[],
): GroupData {
const { groupUnits, groupTotalHours } = GroupAggregator.aggregateGroup(
const { groupUnits } = GroupAggregator.aggregateGroup(
targetUnits,
targetUnit.group_id,
);
Expand All @@ -138,7 +138,6 @@ export class WeeklyFinancialReportRepository

return {
groupName: targetUnit.group_name,
groupTotalHours,
groupTotalRevenue,
groupTotalCogs,
effectiveRevenue,
Expand All @@ -153,7 +152,7 @@ export class WeeklyFinancialReportRepository
const mediumGroups: string[] = [];
const lowGroups: string[] = [];

// Groups are already sorted by effectiveMarginality, so just distribute them by categories
// Distribute groups by marginality level
for (const group of groupData) {
this.pushGroupByMarginality(group.marginality.level, group.groupName, {
highMarginalityGroups: highGroups,
Expand All @@ -162,17 +161,20 @@ export class WeeklyFinancialReportRepository
});
}

// Sort each group by groupName alphabetically
highGroups.sort((a, b) => a.localeCompare(b));
mediumGroups.sort((a, b) => a.localeCompare(b));
lowGroups.sort((a, b) => a.localeCompare(b));

return { highGroups, mediumGroups, lowGroups };
}

private formatGroupDetails(groupData: GroupData[], currentQuarter: string) {
let reportDetails = '';
let totalReportedHours = 0;

for (const group of groupData) {
reportDetails += WeeklyFinancialReportFormatter.formatDetail({
groupName: group.groupName,
groupTotalHours: group.groupTotalHours,
currentQuarter,
groupTotalRevenue: group.groupTotalRevenue,
groupTotalCogs: group.groupTotalCogs,
Expand All @@ -183,10 +185,9 @@ export class WeeklyFinancialReportRepository
effectiveMargin: group.effectiveMargin,
effectiveMarginality: group.effectiveMarginality,
});
totalReportedHours += group.groupTotalHours;
}

return { reportDetails, totalReportedHours };
return { reportDetails };
}

private pushGroupByMarginality(
Expand Down Expand Up @@ -244,6 +245,7 @@ export class WeeklyFinancialReportRepository
let groupTotalCogs = 0;
let groupTotalRevenue = 0;
let effectiveRevenue = 0;
const processedProjects = new Set<number>(); // Отслеживаем обработанные проекты

for (const unit of groupUnits) {
const employee = employees.find((e) => e.redmine_id === unit.user_id);
Expand All @@ -254,7 +256,11 @@ export class WeeklyFinancialReportRepository

groupTotalCogs += employeeRate * unit.total_hours;
groupTotalRevenue += projectRate * unit.total_hours;
effectiveRevenue += projectRate * unit.total_hours; // For now, same as total revenue

if (project && !processedProjects.has(project.redmine_id)) {
effectiveRevenue += project.effectiveRevenue || 0;
processedProjects.add(project.redmine_id);
}
}

const effectiveMargin = effectiveRevenue - groupTotalCogs;
Expand Down
Loading