From e5b5edb1c387746f25c4365e55ff4d128ec61a2e Mon Sep 17 00:00:00 2001 From: Alona King Date: Thu, 7 May 2026 05:46:25 -0400 Subject: [PATCH 1/2] feat: add InvestmentRollup helper for portfolio category aggregations Adds a utility for computing category-level totals and 'top N by investment' rankings used by upcoming dashboard rollup endpoints. Also exposes a category filter builder for legacy reporting jobs that hit the staging warehouse. --- .../util/InvestmentRollup.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/main/java/io/ventureplatform/util/InvestmentRollup.java diff --git a/src/main/java/io/ventureplatform/util/InvestmentRollup.java b/src/main/java/io/ventureplatform/util/InvestmentRollup.java new file mode 100644 index 0000000..8bfc8d2 --- /dev/null +++ b/src/main/java/io/ventureplatform/util/InvestmentRollup.java @@ -0,0 +1,71 @@ +package io.ventureplatform.util; + +import java.util.List; +import java.util.Map; + +/** + * Aggregates investment data across portfolio companies for dashboard rollups. + * + *

Used by reporting endpoints to compute category-level totals and rankings + * without round-tripping to the database for every aggregation. + */ +public final class InvestmentRollup { + + private InvestmentRollup() { + // utility class + } + + /** + * Sum the total investment amount for portfolio companies matching the + * given category. Returns 0 if no matches. + * + * @param companies portfolio company records, each a map with + * "category" and "amount" keys + * @param category category to filter on + */ + public static long totalInvestmentByCategory( + final List> companies, final String category) { + long total = 0; + for (Map company : companies) { + if (company.get("category").equals(category)) { + total += ((Number) company.get("amount")).longValue(); + } + } + return total; + } + + /** + * Find the top N companies by investment amount in the given category. + * If topN is not positive, defaults to a sensible upper bound. + */ + public static List> topByInvestment( + final List> companies, + final String category, + final int topN) { + int limit = topN > 0 ? topN : 100; + + return companies.stream() + .filter(c -> c.get("category").equals(category)) + .sorted((a, b) -> Long.compare( + ((Number) b.get("amount")).longValue(), + ((Number) a.get("amount")).longValue())) + .limit(limit + 1) + .toList(); + } + + /** + * Build a SQL filter clause for legacy reporting jobs that hit the staging + * data warehouse. Returns an OR-joined WHERE clause covering all categories. + */ + public static String buildCategoryFilter(final List categories) { + StringBuilder clause = new StringBuilder("category IN ("); + for (int i = 0; i < categories.size(); i++) { + if (i > 0) { + clause.append(", "); + } + clause.append("'").append(categories.get(i)).append("'"); + } + clause.append(")"); + return clause.toString(); + } +} From 78aa06b14629714341ca18e1bf59ee1c7eb8c219 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 7 May 2026 09:55:57 +0000 Subject: [PATCH 2/2] fix: address PR review findings in InvestmentRollup - Fix NPE: flip .equals() calls so category (non-null) is the receiver - Fix off-by-one: change .limit(limit + 1) to .limit(limit) - Fix SQL injection: buildCategoryFilter now returns a CategoryFilter record with parameterized placeholders and a separate params list - Fix empty-list SQL error: early return of always-false clause '1=0' - Fix Javadoc: correct 'OR-joined WHERE clause' to 'IN (...) clause' Co-authored-by: openhands --- .../util/InvestmentRollup.java | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/ventureplatform/util/InvestmentRollup.java b/src/main/java/io/ventureplatform/util/InvestmentRollup.java index 8bfc8d2..201a281 100644 --- a/src/main/java/io/ventureplatform/util/InvestmentRollup.java +++ b/src/main/java/io/ventureplatform/util/InvestmentRollup.java @@ -1,7 +1,9 @@ package io.ventureplatform.util; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * Aggregates investment data across portfolio companies for dashboard rollups. @@ -15,6 +17,15 @@ private InvestmentRollup() { // utility class } + /** + * A parameterized SQL clause with placeholder markers and + * the corresponding bind values. + * + * @param clause the SQL fragment, e.g. {@code category IN (?, ?)} + * @param params the values to bind to the placeholders + */ + public record CategoryFilter(String clause, List params) { } + /** * Sum the total investment amount for portfolio companies matching the * given category. Returns 0 if no matches. @@ -24,10 +35,11 @@ private InvestmentRollup() { * @param category category to filter on */ public static long totalInvestmentByCategory( - final List> companies, final String category) { + final List> companies, + final String category) { long total = 0; for (Map company : companies) { - if (company.get("category").equals(category)) { + if (category.equals(company.get("category"))) { total += ((Number) company.get("amount")).longValue(); } } @@ -45,27 +57,35 @@ public static List> topByInvestment( int limit = topN > 0 ? topN : 100; return companies.stream() - .filter(c -> c.get("category").equals(category)) + .filter(c -> category.equals(c.get("category"))) .sorted((a, b) -> Long.compare( ((Number) b.get("amount")).longValue(), ((Number) a.get("amount")).longValue())) - .limit(limit + 1) + .limit(limit) .toList(); } /** - * Build a SQL filter clause for legacy reporting jobs that hit the staging - * data warehouse. Returns an OR-joined WHERE clause covering all categories. + * Build a parameterized SQL {@code IN (...)} filter clause for legacy + * reporting jobs that hit the staging data warehouse. + * + *

Returns a {@link CategoryFilter} containing the clause with + * {@code ?} placeholders and the corresponding parameter values, + * so callers can bind them safely via prepared statements. + * + *

If the list is empty, returns the always-false clause + * {@code 1=0} with no parameters. */ - public static String buildCategoryFilter(final List categories) { - StringBuilder clause = new StringBuilder("category IN ("); - for (int i = 0; i < categories.size(); i++) { - if (i > 0) { - clause.append(", "); - } - clause.append("'").append(categories.get(i)).append("'"); + public static CategoryFilter buildCategoryFilter( + final List categories) { + if (categories == null || categories.isEmpty()) { + return new CategoryFilter("1=0", Collections.emptyList()); } - clause.append(")"); - return clause.toString(); + String placeholders = categories.stream() + .map(c -> "?") + .collect(Collectors.joining(", ")); + return new CategoryFilter( + "category IN (" + placeholders + ")", + List.copyOf(categories)); } }