Skip to content

Implement additional widget metrics#20

Open
mgkcloud wants to merge 1 commit intomasterfrom
codex/ensure-v1-metric-widgets-are-production-ready
Open

Implement additional widget metrics#20
mgkcloud wants to merge 1 commit intomasterfrom
codex/ensure-v1-metric-widgets-are-production-ready

Conversation

@mgkcloud
Copy link
Copy Markdown
Owner

@mgkcloud mgkcloud commented May 22, 2025

Summary

  • expose xero tables in DB client
  • compute cash burn, payables, snapshot and other metrics
  • document backend completion for these widgets
  • add vitest covering new metric logic

Testing

  • npm run lint --workspace=packages/backend (fails: ESLint couldn't find configuration)
  • npm run typecheck --workspace=packages/backend
  • npm run test --workspace=packages/backend
  • npm run build --workspace=packages/backend

Summary by CodeRabbit

  • New Features

    • Added backend support for aggregating multiple financial metrics, including cash burn rate, survival time, receivables, payables, gross profit margin, operating expense, direct labour, material spend, and business snapshot.
  • Bug Fixes

    • None.
  • Documentation

    • Updated the roadmap to reflect backend completion of several metrics and clarified the status of related frontend widgets.
  • Tests

    • Introduced comprehensive tests to verify correct aggregation of financial metrics.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2025

Walkthrough

The updates introduce comprehensive aggregation logic for several financial metrics in the backend, supplementing this with new database schema integrations and an extensive test suite. The roadmap documentation is revised to reflect backend completion of these metrics and a shift in focus to frontend widget development and Timeero integration.

Changes

File(s) Change Summary
docs/roadmap.md Updated to mark backend completion (✅) for "Break-Even Revenue" and "Revenue existing vs new" metrics, adjusted widget statuses, revised backend action items, and updated legend/notes.
packages/backend/src/db/index.ts Added imports and integration for xero_expenses, xero_balance_sheet, and xero_profit_and_loss schemas into the database schema aggregation.
packages/backend/src/services/metricsAggregation.ts Implemented aggregation logic for multiple metrics: cashBurnRate, survivalTime, receivables, payables, grossProfitMargin, operatingExpense, directLabour, materialSpend, and businessSnapshot. Added queries and calculations for each metric. Removed TODOs.
packages/backend/src/services/metricsAggregation.test.ts Added a new test suite for aggregateMetric, setting up an in-memory SQLite DB, seeding with sample data, and verifying calculations for cashBurnRate, grossProfitMargin, and businessSnapshot.

Sequence Diagram(s)

sequenceDiagram
    participant TestSuite
    participant InMemoryDB
    participant MetricsAggregator

    TestSuite->>InMemoryDB: Setup schema & seed data
    TestSuite->>MetricsAggregator: aggregateMetric('cashBurnRate', ...)
    MetricsAggregator->>InMemoryDB: Query expenses in date range
    MetricsAggregator-->>TestSuite: Return cashBurnRate value

    TestSuite->>MetricsAggregator: aggregateMetric('grossProfitMargin', ...)
    MetricsAggregator->>InMemoryDB: Query invoices and expenses
    MetricsAggregator-->>TestSuite: Return grossProfitMargin value

    TestSuite->>MetricsAggregator: aggregateMetric('businessSnapshot', ...)
    MetricsAggregator->>InMemoryDB: Query latest balance sheet
    MetricsAggregator-->>TestSuite: Return businessSnapshot value
Loading

Poem

In the warren, numbers crunch,
Metrics hop and widgets munch.
Backend burrows, logic deep,
Tests awake from data sleep.
Roadmaps bloom, the future near—
Rabbits code with fuzzy cheer!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
packages/backend/src/services/metricsAggregation.test.ts (2)

14-121: Test setup is comprehensive but could benefit from reusable fixtures.

The test setup creates an in-memory database with all necessary tables and seeds it with appropriate test data. This is a good approach for testing database interactions.

Consider extracting the database setup into a separate helper function or fixture that could be reused across multiple test files, especially if more tests will be added in the future.

-beforeAll(() => {
+function setupTestDatabase() {
   const sqlite = new Database(':memory:');
   // Table creation and data seeding logic
   // ...
   return drizzle(sqlite, { schema: { xero_invoices, xero_expenses, xero_balance_sheet, metricValues } });
+}

+beforeAll(() => {
+  db = setupTestDatabase();
 });

127-130: Consider more precise assertions for cashBurnRate.

The test verifies that cashBurnRate is greater than 0, which validates basic functionality but may miss calculation errors.

Based on the test data (expenses of 300+100+50=450 over approximately 1 month), a more precise assertion would be:

-expect(result.value).toBeGreaterThan(0);
+expect(result.value).toBeCloseTo(450, 0); // Allow some rounding differences
packages/backend/src/services/metricsAggregation.ts (3)

510-517: Document the meaning of the magic number 4.345.

The survivalTime calculation uses 4.345 to convert from monthly to weekly burn rate, but this isn't immediately obvious without context.

Extract the magic number into a named constant with a comment:

+// Average number of weeks per month (52 weeks / 12 months)
+const WEEKS_PER_MONTH = 4.345;

if (metric === 'survivalTime') {
  const cashResult = await aggregateMetric({ db, metric: 'cashAvailable', start, end });
  const burnResult = await aggregateMetric({ db, metric: 'cashBurnRate', start, end });
  const cash = extractNumericValue(cashResult);
  const burn = extractNumericValue(burnResult);
-  const weeks = burn > 0 ? cash / (burn / 4.345) : 0;
+  const weeks = burn > 0 ? cash / (burn / WEEKS_PER_MONTH) : 0;
  return { value: weeks };
}

533-540: Consider optimizing the grossProfitMargin calculation to reduce database calls.

The current implementation makes two separate database calls (for revenue and grossProfit) that could potentially be combined.

Consider calculating the margin directly from profit and loss data:

if (metric === 'grossProfitMargin') {
-  const revResult = await aggregateMetric({ db, metric: 'revenue', start, end });
-  const profitResult = await aggregateMetric({ db, metric: 'grossProfit', start, end });
-  const revenue = extractNumericValue(revResult);
-  const grossProfit = extractNumericValue(profitResult);
-  const margin = revenue > 0 ? (grossProfit / revenue) * 100 : 0;
+  const latestProfitLoss = await db.query.metricValues.findFirst({
+    where: and(
+       eq(metricValues.metricId, 'xero_profit_loss'),
+       gte(metricValues.recordedAt, isoStart),
+       lte(metricValues.recordedAt, isoEnd)
+    ),
+    orderBy: [desc(metricValues.recordedAt)],
+  });
+  
+  let grossProfit = 0;
+  let revenue = 0;
+  
+  if (latestProfitLoss?.context) {
+    const profitLossData = JSON.parse(latestProfitLoss.context);
+    grossProfit = profitLossData.grossProfit ?? 0;
+    revenue = profitLossData.revenue ?? 0;
+  }
+  
+  const margin = revenue > 0 ? (grossProfit / revenue) * 100 : 0;
  return { value: margin };
}

563-574: Improve error handling and typecasting in businessSnapshot implementation.

The current implementation works but has some verbose typecasting and could handle edge cases more elegantly.

Refactor to improve readability and error handling:

if (metric === 'businessSnapshot') {
  const row = await db.select().from(xero_balance_sheet)
    .where(lte(xero_balance_sheet.asOfDate, isoEnd))
    .orderBy(desc(xero_balance_sheet.asOfDate))
    .limit(1);
  if (row[0]) {
-    const assets = typeof row[0].assets === 'number' ? row[0].assets : Number(row[0].assets) || 0;
-    const liabilities = typeof row[0].liabilities === 'number' ? row[0].liabilities : Number(row[0].liabilities) || 0;
+    // Ensure values are numbers with sensible defaults
+    const assets = Number(row[0].assets) || 0;
+    const liabilities = Number(row[0].liabilities) || 0;
    return { value: assets - liabilities };
  }
  return { value: 0 };
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ba0313 and e9349c7.

📒 Files selected for processing (4)
  • docs/roadmap.md (3 hunks)
  • packages/backend/src/db/index.ts (2 hunks)
  • packages/backend/src/services/metricsAggregation.test.ts (1 hunks)
  • packages/backend/src/services/metricsAggregation.ts (2 hunks)
🔇 Additional comments (14)
packages/backend/src/db/index.ts (2)

10-12: LGTM! Schema imports added for Xero financial data.

These new schema imports provide the database structure for the financial metrics being implemented.


24-26: LGTM! Properly registered schemas in the aggregated schema object.

The schema registrations follow the established pattern in the codebase.

docs/roadmap.md (4)

11-11: LGTM! Documentation update reflects backend implementation of Break-Even Revenue.

The status update correctly reflects that the backend implementation is complete while frontend widget is still pending.


26-26: LGTM! Documentation update reflects backend implementation of Revenue existing vs new.

The status update correctly reflects that the backend implementation is complete while frontend widget is still pending.


57-63: LGTM! Widget implementation status updated accurately.

These entries align with the implemented backend metrics and correctly indicate that widget implementation is pending for the frontend.


70-70: LGTM! Action item updated to focus on Timeero integration.

This update correctly reflects that the backend focus has narrowed to implementing Timeero integration, as the other metrics have been implemented.

packages/backend/src/services/metricsAggregation.test.ts (1)

1-11: LGTM! Test setup imports the necessary dependencies.

The imports include testing libraries, database tools, and the schemas needed for the tests.

packages/backend/src/services/metricsAggregation.ts (7)

8-8: LGTM! Added import for balance sheet schema.

This import is necessary for the businessSnapshot metric implementation.


501-508: LGTM! cashBurnRate implementation.

The implementation correctly calculates the monthly expense rate by summing all expenses in the period and dividing by the number of months.


519-524: LGTM! receivables implementation.

The implementation correctly sums the amount due from authorized invoices in the specified period.


526-531: LGTM! payables implementation.

The implementation correctly sums the amount from unpaid expenses in the specified period.


542-547: LGTM! operatingExpense implementation.

The implementation correctly sums expenses categorized as OPEX in the specified period.


549-554: LGTM! directLabour implementation.

The implementation correctly sums expenses categorized as LABOR in the specified period.


556-561: LGTM! materialSpend implementation.

The implementation correctly sums expenses categorized as MATERIALS in the specified period.

Comment on lines +132 to +140
it('calculates grossProfitMargin', async () => {
const result = await aggregateMetric({ db: db as any, metric: 'grossProfitMargin', start, end });
expect(result.value).toBeCloseTo(60);
});

it('calculates businessSnapshot', async () => {
const result = await aggregateMetric({ db: db as any, metric: 'businessSnapshot', start, end });
expect(result.value).toBe(3000);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Test coverage only includes 3 of 9 implemented metrics.

While the tests for cashBurnRate, grossProfitMargin, and businessSnapshot are a good start, the test suite would be more robust with coverage for all implemented metrics.

Add test cases for the remaining metrics:

it('calculates survivalTime', async () => {
  const result = await aggregateMetric({ db: db as any, metric: 'survivalTime', start, end });
  expect(result.value).toBeGreaterThan(0);
});

it('calculates receivables', async () => {
  const result = await aggregateMetric({ db: db as any, metric: 'receivables', start, end });
  expect(result.value).toBeCloseTo(200); // Based on amountDue in test data
});

it('calculates payables', async () => {
  const result = await aggregateMetric({ db: db as any, metric: 'payables', start, end });
  expect(result.value).toBeCloseTo(300); // Based on UNPAID expenses in test data
});

it('calculates operatingExpense', async () => {
  const result = await aggregateMetric({ db: db as any, metric: 'operatingExpense', start, end });
  expect(result.value).toBeCloseTo(300); // Based on OPEX category in test data
});

it('calculates directLabour', async () => {
  const result = await aggregateMetric({ db: db as any, metric: 'directLabour', start, end });
  expect(result.value).toBeCloseTo(100); // Based on LABOR category in test data
});

it('calculates materialSpend', async () => {
  const result = await aggregateMetric({ db: db as any, metric: 'materialSpend', start, end });
  expect(result.value).toBeCloseTo(50); // Based on MATERIALS category in test data
});
🤖 Prompt for AI Agents
In packages/backend/src/services/metricsAggregation.test.ts around lines 132 to
140, the test suite currently covers only 3 of the 9 implemented metrics. To
improve test coverage, add test cases for the remaining metrics: survivalTime,
receivables, payables, operatingExpense, directLabour, and materialSpend. For
each, call aggregateMetric with the appropriate metric name and assert the
expected value based on the test data, using toBeCloseTo or toBeGreaterThan as
appropriate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant