From afac9baebeef2606c0f88af94f4dc8909494e066 Mon Sep 17 00:00:00 2001 From: Alona King Date: Thu, 7 May 2026 11:19:12 -0400 Subject: [PATCH 1/2] feat: add investment rollup helper for category-level reporting --- .../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 83feb90daad3a10c680bd2f010e2f1a44fc0e9ca Mon Sep 17 00:00:00 2001 From: alona Date: Thu, 14 May 2026 15:57:32 +0000 Subject: [PATCH 2/2] Address review: type-safe rollup, parameterised SQL, fail-fast topN - Replace Map with typed PortfolioInvestment record so callers can't pass missing keys or wrong value types (fixes NPE/CCE risks on amount/category). - Use Objects.equals when comparing categories to avoid NPE on null category values. - Drop the .limit(limit + 1) off-by-one in topByInvestment; return exactly topN items. - Throw IllegalArgumentException instead of silently falling back to a magic 100 when topN <= 0. - buildCategoryFilter now returns a CategoryFilter record carrying a parameterised SQL fragment (category IN (?, ?, ...)) plus the bound values, eliminating the SQL injection vector. Empty/null categories short-circuit to '1=0' so callers don't produce 'category IN ()'. --- .../util/InvestmentRollup.java | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/src/main/java/io/ventureplatform/util/InvestmentRollup.java b/src/main/java/io/ventureplatform/util/InvestmentRollup.java index cd1b8e4..eb71490 100644 --- a/src/main/java/io/ventureplatform/util/InvestmentRollup.java +++ b/src/main/java/io/ventureplatform/util/InvestmentRollup.java @@ -1,7 +1,8 @@ package io.ventureplatform.util; +import java.util.Collections; import java.util.List; -import java.util.Map; +import java.util.Objects; /** * Aggregates investment data across portfolio companies for dashboard rollups. @@ -11,6 +12,19 @@ */ public final class InvestmentRollup { + /** + * 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 params) { } + private InvestmentRollup() { // utility class } @@ -20,48 +34,53 @@ private InvestmentRollup() { * 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 (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. - * If topN is not positive, defaults to a sensible upper bound. + * 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> topByInvestment( - final List> companies, + public static List topByInvestment( + final List companies, final String category, final int topN) { - int limit = topN > 0 ? topN : 100; - + if (topN <= 0) { + throw new IllegalArgumentException("topN must be positive, was " + topN); + } 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) + .filter(c -> Objects.equals(c.category(), category)) + .sorted((a, b) -> Long.compare(b.amount(), a.amount())) + .limit(topN) .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 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. + * + *

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 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", List.of()); } - clause.append(")"); - return clause.toString(); + String placeholders = String.join(", ", + Collections.nCopies(categories.size(), "?")); + return new CategoryFilter( + "category IN (" + placeholders + ")", + List.copyOf(categories)); } }