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.
This commit is contained in:
2026-08-19 10:35:48 +05:30
parent 0e07e60328
commit 5c0a482f49
2 changed files with 69 additions and 0 deletions
@@ -52,13 +52,22 @@ public class MobileLookupServiceImpl implements MobileLookupService {
"9752123456", "9752345678", "9752987654" "9752123456", "9752345678", "9752987654"
); );
/** Demo numbers that always return DISCONNECTED (found on the MNRL) without calling the external API. */
private static final Set<String> DEMO_DISCONNECTED_NUMBERS = Set.of("9000000001", "9000000002");
/** Demo numbers that always return SUSPECTED_AND_DISCONNECTED without calling the external API. */
private static final Set<String> DEMO_SUSPECTED_AND_DISCONNECTED_NUMBERS = Set.of("9000000003");
private final String baseUrl; private final String baseUrl;
private final boolean enabled;
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private final MobileLookupLogRepository logRepository; private final MobileLookupLogRepository logRepository;
public MobileLookupServiceImpl(@Value("${fineract.mobile-lookup.base-url}") String baseUrl, public MobileLookupServiceImpl(@Value("${fineract.mobile-lookup.base-url}") String baseUrl,
@Value("${fineract.mobile-lookup.enabled:true}") boolean enabled,
MobileLookupLogRepository logRepository) { MobileLookupLogRepository logRepository) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.enabled = enabled;
this.restTemplate = new RestTemplate(); this.restTemplate = new RestTemplate();
this.logRepository = logRepository; this.logRepository = logRepository;
} }
@@ -96,6 +105,39 @@ public class MobileLookupServiceImpl implements MobileLookupService {
return MobileLookupResult.clean(logId, trimmed); 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; final String url = baseUrl + "/api/v1/lookup/check?mobileNumber=" + trimmed;
LOG.info("Calling DIP/MNRL lookup API: {}", url); LOG.info("Calling DIP/MNRL lookup API: {}", url);
@@ -289,6 +331,26 @@ public class MobileLookupServiceImpl implements MobileLookupService {
return false; 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) { private String getStringOrNull(JsonObject json, String key) {
JsonElement el = json.get(key); JsonElement el = json.get(key);
return (el != null && !el.isJsonNull()) ? el.getAsString() : null; return (el != null && !el.isJsonNull()) ? el.getAsString() : null;
@@ -688,6 +688,13 @@ fineract.openproject.api-token=${FINERACT_OPENPROJECT_API_TOKEN:57dd9f933f113c72
# │ DIP/MNRL provider. Do not ship the mock. │ # │ DIP/MNRL provider. Do not ship the mock. │
# └──────────────────────────────────────────────────────────────────────────┘ # └──────────────────────────────────────────────────────────────────────────┘
fineract.mobile-lookup.base-url=${FINERACT_MOBILE_LOOKUP_BASE_URL:http://127.0.0.1:8090} 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 ───────────────────────── # ── Login OTP (2FA) — uses LincPay credentials above ─────────────────────────
fineract.security.otp.lincpay.generate-path=${FINERACT_OTP_LINCPAY_GENERATE_PATH:/mobile/generate-otp} fineract.security.otp.lincpay.generate-path=${FINERACT_OTP_LINCPAY_GENERATE_PATH:/mobile/generate-otp}