From 5c0a482f4926497f7c9827e8def3010fda97ee9f Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Wed, 19 Aug 2026 10:35:48 +0530 Subject: [PATCH] feat(kyc): resolve DIP/MNRL demo verdicts locally, make the check toggleable Every mobile-number lookup was calling out to a local mock HTTP service on port 8090 that had to be started separately, so the check failed with a connection error whenever that process wasn't running. Ports the mock's full verdict table (suspected, disconnected, suspected+disconnected, clean) into MobileLookupServiceImpl itself, so the demo/test numbers resolve in-process with no HTTP call. Adds fineract.mobile-lookup.enabled (default off) to skip the real DIP/MNRL call entirely for local work; every number then resolves CLEAN and is stamped LOOKUP_DISABLED_BY_CONFIG in the log so it's never mistaken for a genuine pass. Must be turned back on before UAT/production. --- .../service/MobileLookupServiceImpl.java | 62 +++++++++++++++++++ .../src/main/resources/application.properties | 7 +++ 2 files changed, 69 insertions(+) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/mobilelookup/service/MobileLookupServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/mobilelookup/service/MobileLookupServiceImpl.java index 92878b569..bed72df0d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/mobilelookup/service/MobileLookupServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/mobilelookup/service/MobileLookupServiceImpl.java @@ -52,13 +52,22 @@ public class MobileLookupServiceImpl implements MobileLookupService { "9752123456", "9752345678", "9752987654" ); + /** Demo numbers that always return DISCONNECTED (found on the MNRL) without calling the external API. */ + private static final Set DEMO_DISCONNECTED_NUMBERS = Set.of("9000000001", "9000000002"); + + /** Demo numbers that always return SUSPECTED_AND_DISCONNECTED without calling the external API. */ + private static final Set DEMO_SUSPECTED_AND_DISCONNECTED_NUMBERS = Set.of("9000000003"); + private final String baseUrl; + private final boolean enabled; private final RestTemplate restTemplate; private final MobileLookupLogRepository logRepository; public MobileLookupServiceImpl(@Value("${fineract.mobile-lookup.base-url}") String baseUrl, + @Value("${fineract.mobile-lookup.enabled:true}") boolean enabled, MobileLookupLogRepository logRepository) { this.baseUrl = baseUrl; + this.enabled = enabled; this.restTemplate = new RestTemplate(); this.logRepository = logRepository; } @@ -96,6 +105,39 @@ public class MobileLookupServiceImpl implements MobileLookupService { return MobileLookupResult.clean(logId, trimmed); } + // Demo numbers — return DISCONNECTED (on the MNRL) without calling the external API + if (isDemoDisconnectedNumber(trimmed)) { + LOG.info("Demo disconnected number detected: {}", trimmed); + final MobileLookupDetailData disconnectedDetail = MobileLookupDetailData.forDisconnected( + "Number revoked and listed on MNRL (demo data)", "DEMO"); + final Long logId = saveLog(trimmed, "DISCONNECTED", false, true, + "Number is DISCONNECTED — found in MNRL records", null, source, clientId); + return MobileLookupResult.disconnected(logId, trimmed, disconnectedDetail); + } + + // Demo numbers — return SUSPECTED_AND_DISCONNECTED without calling the external API + if (isDemoSuspectedAndDisconnectedNumber(trimmed)) { + LOG.info("Demo suspected+disconnected number detected: {}", trimmed); + final MobileLookupDetailData suspectedDetail = MobileLookupDetailData.forSuspected( + "active", "Demo DIP suspected number", "DEMO"); + final MobileLookupDetailData disconnectedDetail = MobileLookupDetailData.forDisconnected( + "Number revoked and listed on MNRL (demo data)", "DEMO"); + final Long logId = saveLog(trimmed, "SUSPECTED_AND_DISCONNECTED", true, true, + "Number is SUSPECTED and DISCONNECTED — found in both DIP and MNRL records", null, source, clientId); + return MobileLookupResult.suspectedAndDisconnected(logId, trimmed, suspectedDetail, disconnectedDetail); + } + + // Lookup switched off by config (fineract.mobile-lookup.enabled=false) -- skip the external + // call entirely rather than fail with a connection error. Logged distinctly from a genuine + // CLEAN result so it can never be mistaken for one in an audit trail. + if (!enabled) { + LOG.info("DIP/MNRL lookup is disabled by configuration — skipping check for {}", trimmed); + final Long logId = saveLog(trimmed, MobileLookupLog.STATUS_CLEAN, false, false, + "DIP/MNRL lookup is disabled by configuration — the number was not actually checked.", + "{\"provider\":\"LOOKUP_DISABLED_BY_CONFIG\"}", source, clientId); + return MobileLookupResult.clean(logId, trimmed); + } + final String url = baseUrl + "/api/v1/lookup/check?mobileNumber=" + trimmed; LOG.info("Calling DIP/MNRL lookup API: {}", url); @@ -289,6 +331,26 @@ public class MobileLookupServiceImpl implements MobileLookupService { return false; } + private boolean isDemoDisconnectedNumber(final String mobileNumber) { + if (DEMO_DISCONNECTED_NUMBERS.contains(mobileNumber)) { + return true; + } + if (mobileNumber.startsWith("91") && mobileNumber.length() > 10) { + return DEMO_DISCONNECTED_NUMBERS.contains(mobileNumber.substring(2)); + } + return false; + } + + private boolean isDemoSuspectedAndDisconnectedNumber(final String mobileNumber) { + if (DEMO_SUSPECTED_AND_DISCONNECTED_NUMBERS.contains(mobileNumber)) { + return true; + } + if (mobileNumber.startsWith("91") && mobileNumber.length() > 10) { + return DEMO_SUSPECTED_AND_DISCONNECTED_NUMBERS.contains(mobileNumber.substring(2)); + } + return false; + } + private String getStringOrNull(JsonObject json, String key) { JsonElement el = json.get(key); return (el != null && !el.isJsonNull()) ? el.getAsString() : null; diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 18fcdd30f..e39a6cdd6 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -688,6 +688,13 @@ fineract.openproject.api-token=${FINERACT_OPENPROJECT_API_TOKEN:57dd9f933f113c72 # │ DIP/MNRL provider. Do not ship the mock. │ # └──────────────────────────────────────────────────────────────────────────┘ fineract.mobile-lookup.base-url=${FINERACT_MOBILE_LOOKUP_BASE_URL:http://127.0.0.1:8090} +# Set to false to skip the DIP/MNRL check entirely (no HTTP call at all, no need to run the mock +# service) -- every number is then logged as CLEAN but stamped "LOOKUP_DISABLED_BY_CONFIG" so it is +# never mistaken for a real pass. +# Currently OFF by default for local work so nobody needs the mock service running. Set +# FINERACT_MOBILE_LOOKUP_ENABLED=true (or flip the default below) before UAT/production, where the +# check must actually run. +fineract.mobile-lookup.enabled=${FINERACT_MOBILE_LOOKUP_ENABLED:false} # ── Login OTP (2FA) — uses LincPay credentials above ───────────────────────── fineract.security.otp.lincpay.generate-path=${FINERACT_OTP_LINCPAY_GENERATE_PATH:/mobile/generate-otp}