Skip to content
Open
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
86 changes: 86 additions & 0 deletions src/main/java/io/ventureplatform/util/InvestmentRollup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.ventureplatform.util;

import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
* Aggregates investment data across portfolio companies for dashboard rollups.
*
* <p>Used by reporting endpoints to compute category-level totals and rankings
* without round-tripping to the database for every aggregation.
*/
public final class InvestmentRollup {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🧹 No unit tests. None of totalInvestmentByCategory, topByInvestment, or buildCategoryFilter have tests, and the repo has no src/test/java/io/ventureplatform/ directory at all yet. For a pure utility class like this — explicitly described in the PR body as a "building block" — tests are cheap and would have caught the off-by-one on line 48 and the empty-list case on line 57 before review.

Suggested coverage: happy path with mixed categories; empty companies / empty categories; matching category but no amount key; topN <= 0; ensure topByInvestment returns exactly topN items sorted descending; and a regression test for the null-category-value case once that's fixed.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Not addressed in this PR. The repo has no test infrastructure yet — pom.xml has no spring-boot-starter-test/JUnit dependency and there is no src/test/java/ tree at all (confirmed via grep -i junit pom.xml and find src -type d). Per the project AGENTS.md, bringing up a test harness from zero is out of scope for a single-comment fix and should be a separate change; leaving this thread open as a tracker. The specific bugs the suggested tests would have caught (off-by-one, empty-list SQL, null category, missing/wrong-type amount) are fixed in 83feb90 by tightening the types and the helper contracts.


/**
* Typed input for rollup calculations. Callers map their domain data
* (entities, DTOs, query rows) to this record at the boundary.
*/
public record PortfolioInvestment(String category, long amount) { }

/**
* Result of {@link #buildCategoryFilter(List)}: a parameterised SQL fragment
* and the bound parameter values, ready to be passed to a
* {@code PreparedStatement} or {@code JdbcTemplate}.
*/
public record CategoryFilter(String sql, List<String> params) { }

private InvestmentRollup() {
// utility class
}

/**
* Sum the total investment amount for portfolio companies matching the
* given category. Returns 0 if no matches.
*/
public static long totalInvestmentByCategory(
final List<PortfolioInvestment> companies, final String category) {
long total = 0;
for (PortfolioInvestment company : companies) {
if (Objects.equals(company.category(), category)) {
total += company.amount();
}
}
return total;
}

/**
* Find the top N companies by investment amount in the given category,
* sorted descending by amount.
*
* @throws IllegalArgumentException if {@code topN} is not positive
*/
public static List<PortfolioInvestment> topByInvestment(
final List<PortfolioInvestment> companies,
final String category,
final int topN) {
if (topN <= 0) {
throw new IllegalArgumentException("topN must be positive, was " + topN);
}
return companies.stream()
.filter(c -> Objects.equals(c.category(), category))
.sorted((a, b) -> Long.compare(b.amount(), a.amount()))
.limit(topN)
.toList();
}

/**
* Build a parameterised SQL filter for the given categories. The returned
* fragment uses {@code ?} placeholders so callers can bind values through a
* prepared statement and avoid SQL injection.
*
* <p>If {@code categories} is null or empty, returns a clause that matches
* nothing ({@code 1=0}) with no parameters, so callers can compose it
* unconditionally without producing invalid SQL.
*/
public static CategoryFilter buildCategoryFilter(final List<String> categories) {
if (categories == null || categories.isEmpty()) {
return new CategoryFilter("1=0", List.of());
}
String placeholders = String.join(", ",
Collections.nCopies(categories.size(), "?"));
return new CategoryFilter(
"category IN (" + placeholders + ")",
List.copyOf(categories));
}
}
Loading