diff --git a/pom.xml b/pom.xml index edd55598..bb4ef358 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 3.6.1 - 1.18.30 + 1.18.36 2.19.2 3.0.2 @@ -249,6 +249,12 @@ + + + org.apache.maven.plugins + maven-compiler-plugin + + @@ -259,11 +265,15 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 ${java.version} ${java.version} true + full + + -proc:full + org.projectlombok diff --git a/services/am-analysis/pom.xml b/services/am-analysis/pom.xml index 8058b660..cbe18358 100644 --- a/services/am-analysis/pom.xml +++ b/services/am-analysis/pom.xml @@ -133,7 +133,6 @@ org.projectlombok lombok - provided @@ -181,6 +180,7 @@ + org.springframework.boot spring-boot-maven-plugin diff --git a/services/am-analysis/src/main/java/com/am/analysis/service/DashboardAnalysisService.java b/services/am-analysis/src/main/java/com/am/analysis/service/DashboardAnalysisService.java index 763e6491..36564b9b 100644 --- a/services/am-analysis/src/main/java/com/am/analysis/service/DashboardAnalysisService.java +++ b/services/am-analysis/src/main/java/com/am/analysis/service/DashboardAnalysisService.java @@ -57,15 +57,12 @@ public class DashboardAnalysisService { private final AnalysisBusinessMetrics businessMetrics; public DashboardSummary getSummary(String userId) { - return snapshotService.load(userId, DashboardWidgetType.SUMMARY, DashboardSummary.class) - .orElseGet(() -> { - log.info("[Summary] Snapshot miss for user {}, computing live", userId); - DashboardSummary summary = aggregator.getOverallSummary(userId); - if (summary != null) { - snapshotService.persist(userId, DashboardWidgetType.SUMMARY, summary); - } - return summary; - }); + log.info("[Summary] Computing live summary for user {}", userId); + DashboardSummary summary = aggregator.getOverallSummary(userId); + if (summary != null) { + snapshotService.persist(userId, DashboardWidgetType.SUMMARY, summary); + } + return summary; } public List getPortfolioOverviews(String userId) { @@ -205,10 +202,6 @@ private ActivityItem mapHoldingToActivity(AnalysisHolding holding, String portfo : holding.getIdentity().getName(); } - String companyName = StringUtils.hasText(holding.getIdentity().getCompanyName()) - ? holding.getIdentity().getCompanyName() - : holding.getIdentity().getName(); - String symbol = storedSymbol; if (LivePriceOverlayHelper.looksLikeIsin(storedSymbol) && isinToTicker != null) { String ticker = isinToTicker.get(storedSymbol.trim().toUpperCase()); @@ -217,10 +210,10 @@ private ActivityItem mapHoldingToActivity(AnalysisHolding holding, String portfo } if (StringUtils.hasText(ticker)) { symbol = ticker; - } else if (StringUtils.hasText(companyName)) { - symbol = companyName; } } + + String companyName = resolveActivityCompanyName(holding, symbol); String exchange = holding.getIdentity().getExchange(); String sector = holding.getClassification() != null ? holding.getClassification().getSector() : null; @@ -503,6 +496,24 @@ public void publishDashboardMovers(String userId, Map liv } } + private String resolveActivityCompanyName(AnalysisHolding holding, String symbol) { + if (holding == null || holding.getIdentity() == null) { + return null; + } + if (StringUtils.hasText(holding.getIdentity().getCompanyName()) + && !AnalysisAggregator.looksLikeIsin(holding.getIdentity().getCompanyName())) { + return holding.getIdentity().getCompanyName(); + } + if (StringUtils.hasText(holding.getIdentity().getName()) + && !AnalysisAggregator.looksLikeIsin(holding.getIdentity().getName())) { + return holding.getIdentity().getName(); + } + if (symbol != null && !AnalysisAggregator.looksLikeIsin(symbol)) { + return symbol; + } + return null; + } + @lombok.Data @lombok.AllArgsConstructor @lombok.NoArgsConstructor diff --git a/services/am-analysis/src/main/java/com/am/analysis/service/LivePriceOverlayHelper.java b/services/am-analysis/src/main/java/com/am/analysis/service/LivePriceOverlayHelper.java index 3e3fb56f..0c1f5643 100644 --- a/services/am-analysis/src/main/java/com/am/analysis/service/LivePriceOverlayHelper.java +++ b/services/am-analysis/src/main/java/com/am/analysis/service/LivePriceOverlayHelper.java @@ -35,6 +35,38 @@ public static boolean looksLikeIsin(String value) { return trimmed.length() == 12 && trimmed.matches("[A-Z]{2}[A-Z0-9]{10}"); } + /** + * Trading symbol stored on the holding in Mongo (portfolio/watchlist), not a name guess. + * Used when the quote key is an ISIN and identity.symbol is already the NSE ticker. + */ + public static String storedTradingSymbolFromHoldings(String isinOrSymbol, Collection entities) { + if (isinOrSymbol == null || entities == null || !looksLikeIsin(isinOrSymbol)) { + return null; + } + String target = isinOrSymbol.trim(); + for (AnalysisEntity entity : entities) { + if (entity == null || entity.getHoldings() == null) { + continue; + } + for (AnalysisHolding h : entity.getHoldings()) { + if (h == null || h.getIdentity() == null) { + continue; + } + String holdingSymbol = h.getIdentity().getSymbol(); + String holdingIsin = h.getIdentity().getIsin(); + boolean matches = target.equalsIgnoreCase(holdingSymbol) + || (holdingIsin != null && target.equalsIgnoreCase(holdingIsin)); + if (!matches) { + continue; + } + if (holdingSymbol != null && !looksLikeIsin(holdingSymbol)) { + return holdingSymbol.trim().toUpperCase(Locale.ROOT); + } + } + } + return null; + } + /** Infer average buy when Mongo holdings omit averagePrice. */ public static double inferAveragePrice(InvestmentStats inv) { if (inv == null) { diff --git a/services/am-analysis/src/main/java/com/am/analysis/service/aggregator/AnalysisAggregator.java b/services/am-analysis/src/main/java/com/am/analysis/service/aggregator/AnalysisAggregator.java index 9fec0c2a..02bbbbaf 100644 --- a/services/am-analysis/src/main/java/com/am/analysis/service/aggregator/AnalysisAggregator.java +++ b/services/am-analysis/src/main/java/com/am/analysis/service/aggregator/AnalysisAggregator.java @@ -468,9 +468,9 @@ public Map fetchLiveTicksForEntities(List tickersToFetch.add(resolved); } else { tickersToFetch.add(sym); - String fallback = resolveFallbackTicker(sym, entities); - if (fallback != null) { - tickersToFetch.add(fallback); + String storedTicker = LivePriceOverlayHelper.storedTradingSymbolFromHoldings(sym, entities); + if (storedTicker != null) { + tickersToFetch.add(storedTicker); } } } @@ -491,13 +491,13 @@ public Map fetchLiveTicksForEntities(List Map result = new HashMap<>(); for (String sym : symbols) { String ticker = isinToTicker.getOrDefault(sym, sym); - String fallback = resolveFallbackTicker(sym, entities); + String storedTicker = LivePriceOverlayHelper.storedTradingSymbolFromHoldings(sym, entities); Object quoteData = quotesMap.get(ticker); if (quoteData == null) quoteData = quotesMap.get(sym); - if (quoteData == null && fallback != null) - quoteData = quotesMap.get(fallback); + if (quoteData == null && storedTicker != null) + quoteData = quotesMap.get(storedTicker); if (quoteData instanceof Map qData) { Double price = qData.get("lastPrice") != null ? ((Number) qData.get("lastPrice")).doubleValue() @@ -512,12 +512,12 @@ public Map fetchLiveTicksForEntities(List } if (price != null && price > 0) { LivePriceTick liveTick = new LivePriceTick(price, prev); - log.info("[Aggregator] Put liveTick for sym={}, ticker={}, fallback={} -> {}", sym, ticker, - fallback, liveTick); + log.info("[Aggregator] Put liveTick for sym={}, ticker={}, storedTicker={} -> {}", sym, ticker, + storedTicker, liveTick); result.put(sym, liveTick); result.put(ticker, liveTick); - if (fallback != null) - result.put(fallback, liveTick); + if (storedTicker != null) + result.put(storedTicker, liveTick); } else { log.warn("[Aggregator] Price was null or <= 0 for sym={}", sym); } @@ -534,6 +534,10 @@ public Map fetchLiveTicksForEntities(List return Map.of(); } + /** + * Resolves ISINs extracted from a list of complex AnalysisEntity objects (by searching through + * their nested holdings) to their corresponding readable stock ticker symbols (e.g. INFY). + */ public Map resolveIsinDisplayTickers(List entities) { if (entities == null || entities.isEmpty()) { return Map.of(); @@ -565,11 +569,35 @@ public Map resolveIsinDisplayTickers(List entiti } } - private String resolveFallbackTicker(String isin, List entities) { - // All ETFs, bonds, and standard securities are now resolved dynamically via - // MongoDB and Upstox ISIN matching. - // We no longer need hardcoded mappings inside the Java code. - return null; + + /** + * Checks if a symbol string matches the standard 12-character alphanumeric ISIN format (e.g., INE009A01021). + */ + public static boolean looksLikeIsin(String symbol) { + return symbol != null && symbol.length() == 12 && symbol.matches("[A-Z]{2}[A-Z0-9]{10}"); + } + + /** + * Resolves a flat list of raw String symbols (filtering out those that are not ISINs) + * to their corresponding readable stock ticker symbols. + */ + public Map resolveIsinToTickerMap(List symbols) { + if (symbols == null || symbols.isEmpty()) { + return Map.of(); + } + List isins = symbols.stream() + .filter(AnalysisAggregator::looksLikeIsin) + .distinct() + .toList(); + if (isins.isEmpty()) { + return Map.of(); + } + try { + return marketDataClientService.resolveIsinsToTickers(isins); + } catch (Exception e) { + log.warn("[Aggregator] ISIN resolution failed: {}", e.getMessage()); + return Map.of(); + } } /** diff --git a/services/am-analysis/src/main/java/com/am/analysis/service/impl/TopMoversAnalysisService.java b/services/am-analysis/src/main/java/com/am/analysis/service/impl/TopMoversAnalysisService.java index 8955c3d3..abb5839d 100644 --- a/services/am-analysis/src/main/java/com/am/analysis/service/impl/TopMoversAnalysisService.java +++ b/services/am-analysis/src/main/java/com/am/analysis/service/impl/TopMoversAnalysisService.java @@ -274,9 +274,9 @@ private Map buildTicksFromLiveQuotes( tickersToFetch.add(resolved); } else { tickersToFetch.add(sym); - String fallback = resolveFallbackTicker(sym, portfolios); - if (fallback != null) { - tickersToFetch.add(fallback); + String storedTicker = LivePriceOverlayHelper.storedTradingSymbolFromHoldings(sym, portfolios); + if (storedTicker != null) { + tickersToFetch.add(storedTicker); } } } @@ -303,13 +303,13 @@ private Map buildTicksFromLiveQuotes( // Step 4: Build ticks and apply dayChange% directly to holdings for (String originalSym : holdingSymbols) { String ticker = isinToTicker.getOrDefault(originalSym, originalSym); - String fallback = resolveFallbackTicker(originalSym, portfolios); + String storedTicker = LivePriceOverlayHelper.storedTradingSymbolFromHoldings(originalSym, portfolios); Object quoteObj = quotes.get(ticker); if (quoteObj == null) { quoteObj = quotes.get(originalSym); } - if (quoteObj == null && fallback != null) { - quoteObj = quotes.get(fallback); + if (quoteObj == null && storedTicker != null) { + quoteObj = quotes.get(storedTicker); } if (quoteObj instanceof Map) { @@ -329,8 +329,8 @@ private Map buildTicksFromLiveQuotes( LivePriceTick tick = new LivePriceTick(lastPrice, prevClose != null ? prevClose : lastPrice); result.put(originalSym, tick); result.put(ticker, tick); - if (fallback != null) { - result.put(fallback, tick); + if (storedTicker != null) { + result.put(storedTicker, tick); } // Directly stamp dayChange% and live prices onto holdings applyDayChangeToHoldings(portfolios, originalSym, ticker, @@ -345,30 +345,6 @@ private Map buildTicksFromLiveQuotes( return result; } - private String resolveFallbackTicker(String isin, List portfolios) { - if (isin == null || !isin.startsWith("IN") || portfolios == null) return null; - for (AnalysisEntity entity : portfolios) { - if (entity.getHoldings() == null) continue; - for (com.am.analysis.adapter.model.AnalysisHolding h : entity.getHoldings()) { - if (h.getIdentity() != null && isin.equalsIgnoreCase(h.getIdentity().getSymbol())) { - String name = h.getIdentity().getCompanyName(); - if (name != null) { - if (name.contains("GOLDBONDS2029SR-VIII") || name.contains("GOLDBONDS")) return "SGBD29VIII"; - if (name.contains("- HEALTHY")) return "HEALTHY"; - if (name.contains("- GROWWDEFNC")) return "GROWWDEFNC"; - if (name.contains("- GROWWRAIL")) return "GROWWRAIL"; - if (name.contains("- MOHEALTH")) return "MOHEALTH"; - if (name.contains("GOLD BEES") || name.contains("GOLDBEES")) return "GOLDBEES"; - if (name.contains("NIFTY BEES") || name.contains("NIFTYBEES")) return "NIFTYBEES"; - if (name.contains("VODAFONE IDEA") || name.contains("VODAFONE IDEA-EQ")) return "IDEA"; - if (name.contains("RAIL VIKAS") || name.contains("RVNL")) return "RVNL"; - } - } - } - } - return null; - } - /** Stamps live market stats directly onto matched holdings (by ISIN or ticker). */ private void applyDayChangeToHoldings(List portfolios, String isinKey, String tickerKey, diff --git a/services/am-analysis/src/test/java/com/am/analysis/service/LivePriceOverlayHelperTest.java b/services/am-analysis/src/test/java/com/am/analysis/service/LivePriceOverlayHelperTest.java index a8e14d7e..4cea4c11 100644 --- a/services/am-analysis/src/test/java/com/am/analysis/service/LivePriceOverlayHelperTest.java +++ b/services/am-analysis/src/test/java/com/am/analysis/service/LivePriceOverlayHelperTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; class LivePriceOverlayHelperTest { @@ -24,6 +25,25 @@ void looksLikeIsin_detectsStandardIsin() { assertEquals(false, LivePriceOverlayHelper.looksLikeIsin(null)); } + @Test + void storedTradingSymbolFromHoldings_usesMongoTickerNotCompanyName() { + AnalysisHolding holding = AnalysisHolding.builder() + .identity(HoldingIdentity.builder() + .symbol("GROWWDEFNC") + .isin("INF666M01IO8") + .companyName("should-not-be-parsed") + .build()) + .build(); + AnalysisEntity entity = AnalysisEntity.builder() + .type(AnalysisEntityType.PORTFOLIO) + .holdings(List.of(holding)) + .build(); + + assertEquals("GROWWDEFNC", + LivePriceOverlayHelper.storedTradingSymbolFromHoldings("INF666M01IO8", List.of(entity))); + assertNull(LivePriceOverlayHelper.storedTradingSymbolFromHoldings("INF666M01IO8", List.of())); + } + @Test void resolveTick_matchesPrefixedSymbol() { Map ticks = Map.of("ITC", new LivePriceTick(289.28, 289.85));