From 9deb107cdca88deff0e59bac7c1c66ace95943da Mon Sep 17 00:00:00 2001 From: Alona King Date: Thu, 7 May 2026 07:16:49 -0400 Subject: [PATCH 1/2] feat: portfolio rollup helper for category-level reporting Adds a small utility for category-level investment totals, top-N rankings, and a filter clause builder used by upcoming dashboard endpoints. --- .../util/InvestmentRollup.java | 67 +++++++++++++++++++ 1 file changed, 67 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..cd1b8e4 --- /dev/null +++ b/src/main/java/io/ventureplatform/util/InvestmentRollup.java @@ -0,0 +1,67 @@ +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. + */ + 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 70093973a28f7626ef5f3a9d45d035d589f77337 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 7 May 2026 11:28:30 +0000 Subject: [PATCH 2/2] fix: address review feedback on InvestmentRollup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce CompanyInvestment record to replace Map, eliminating NPE and ClassCastException risks on category/amount access - Introduce CategoryFilter record returning parameterized SQL (? placeholders) with bind values instead of interpolating strings (SQL injection fix) - Fix off-by-one: .limit(limit + 1) → .limit(limit) - Handle empty/null categories list with 1=0 sentinel - Use Objects.equals() for null-safe category comparison Co-authored-by: openhands --- .../util/InvestmentRollup.java | 91 +++++++++++++------ 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/src/main/java/io/ventureplatform/util/InvestmentRollup.java b/src/main/java/io/ventureplatform/util/InvestmentRollup.java index cd1b8e4..57615df 100644 --- a/src/main/java/io/ventureplatform/util/InvestmentRollup.java +++ b/src/main/java/io/ventureplatform/util/InvestmentRollup.java @@ -1,13 +1,17 @@ package io.ventureplatform.util; +import java.util.Collections; import java.util.List; -import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; /** - * Aggregates investment data across portfolio companies for dashboard rollups. + * 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. + *

Used by reporting endpoints to compute category-level + * totals and rankings without round-tripping to the database + * for every aggregation. */ public final class InvestmentRollup { @@ -16,52 +20,81 @@ private InvestmentRollup() { } /** - * Sum the total investment amount for portfolio companies matching the - * given category. Returns 0 if no matches. + * Typed representation of a company investment entry. + * + * @param category investment category + * @param amount investment amount + * @param name company name + */ + public record CompanyInvestment( + String category, long amount, String name) { } + + /** + * Parameterized SQL filter with bind values. + * + * @param clause SQL clause with positional placeholders + * @param parameters bind values for the placeholders + */ + public record CategoryFilter( + String clause, List parameters) { } + + /** + * Sum the total investment amount for portfolio companies + * matching the given category. Returns 0 if no matches. */ 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)) { - total += ((Number) company.get("amount")).longValue(); + for (CompanyInvestment company : companies) { + if (Objects.equals(category, company.category())) { + total += company.amount(); } } 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. + * 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, + 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)) + .filter(c -> Objects.equals( + category, c.category())) .sorted((a, b) -> Long.compare( - ((Number) b.get("amount")).longValue(), - ((Number) a.get("amount")).longValue())) - .limit(limit + 1) + b.amount(), a.amount())) + .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 filter clause for legacy + * reporting jobs. Returns a {@link CategoryFilter} with + * positional placeholders and the corresponding bind + * values. + * + *

Returns {@code 1=0} with an empty parameter list + * when the input is empty. */ - 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(", ")); + String clause = + "category IN (" + placeholders + ")"; + return new CategoryFilter( + clause, List.copyOf(categories)); } }