Stage 2 — OTP Verification & Background Check Pipeline
2.1 OTP Verification
Validate the 4-digit OTP submitted by the customer against the in-memory store. Enforce attempt limits, resend limits, and cooldown rules.
BRD / PRD Requirement PRD
BRD Stage 2 — OTP_VERIFIED: 4-digit OTP validated against in-memory store. Customer auto-submits on 4th digit entry.
- Max 5 wrong attempts → lead state = DROPPED
- Auto-submit on 4th digit (frontend behavior)
- Resend max 3 times within 30 minutes
- 30-second cooldown between resend requests
- OTP never stored in database — in-memory only
Old System OLD
- SP:
RESUME_AUTHENTICATE_USER_OTP_PASS — validated OTP stored in database
- SP:
USP_VALIDATE_UPDATE_OTP_MOBILE_SJET — updated attempt counters
- Called from:
LoginRepository.AuthenticateLogin()
- OTP Storage: OTP value stored in database (persisted)
- Files:
Joruney_imp_code\Registration\LoginRepository.cs, LoginController.cs
New System NEW
- Service:
OtpVerificationService.VerifyOtpAsync()
- OTP Retrieval:
InMemoryOtpStore.Get(lead.Mobile, "MOBILE")
- Config: MaxWrongAttempts=5, MaxResendCount=3, MinResendIntervalSeconds=30
- OTP Storage: OTP NEVER in database — ConcurrentDictionary in-memory only
- Tracking:
OtpVerification entity tracks attempt counts and resend counts (but not the OTP value itself)
- File:
Services/Stage2/OtpVerificationService.cs
Key Difference: Old system stored OTP in database and validated via stored procedure. New system stores OTP exclusively in-memory (ConcurrentDictionary) — OTP value never touches the database. Attempt tracking moved from SP to OtpVerification entity.
2.2 RM Resume OTP Bypass
When an RM resumes a journey from Zoho CRM, OTP verification is bypassed entirely. Lead transitions directly to OTP_VERIFIED.
BRD / PRD Requirement PRD
BRD — RM Resume: When RM resumes a dropped-off journey from Zoho, OTP is bypassed entirely. The lead state transitions directly to OTP_VERIFIED without any OTP exchange.
- Applies only to RM-assisted Zoho resume (not franchise users)
- Background checks still fire after bypass
Old System OLD
- SP:
USP_INSERT_UPDATE_MOBILE_EMAIL_OTP_BYPASS_SJET — franchise bypass mechanism
- Table:
TBL_MOBILE_EMAIL_OTP_BYPASS_DETAILS — ISMOBILEBYPASS / ISEMAILBYPASS flags
- Scope: Generic franchise bypass, not RM-specific
New System NEW
- Service:
OtpVerificationService checks lead.IsRmResumeBypass
- If true: Skips OTP validation, transitions to OTP_VERIFIED, fires background checks
- Audit: State transition logged with TriggerAction = "RM_RESUME_BYPASS"
- File:
Services/Stage2/OtpVerificationService.cs
Key Difference: Old system had a generic franchise bypass via a dedicated bypass table. New system uses a boolean flag (IsRmResumeBypass) on the lead entity, checked inline by OtpVerificationService. Audit trail records the bypass action with a specific trigger.
2.3 Background Check Pipeline (Post-OTP Verified)
After OTP is verified, fire all background checks asynchronously. Customer proceeds to Stage 3 immediately without waiting.
BRD / PRD Requirement PRD
BRD — Background Pipeline: Ordered async pipeline: Zintlr → (Hyperverge + C-safe parallel) → (NSDL + CVL KRA parallel). UTI fallback only when NSDL is DOWN. All async — customer moves to Stage 3 immediately.
- Step 1: Zintlr phone-to-PAN lookup (plain mobile)
- Step 2: Hyperverge PAN details + C-safe AML check (parallel)
- Step 3: NSDL PAN validation + CVL KRA status (parallel)
- UTI is fallback for NSDL only — triggered when NSDL is down
- Customer does NOT wait for any of these checks
Old System OLD
- Timing: PAN check happened at PAN stage (Stage 4), NOT after OTP. Not a unified pipeline.
- PAN APIs:
PanAPI.VerifyPAN_NSDL(), PanAPI.GetPanDetailsHyperverge(), PanAPI.CallMobileToPanDetailsAsync()
- C-safe:
USP_INSERT_CSAFE_Req_Resp
- KRA:
USP_INSERT_KRA_LOGS_SJET
- Execution: All sequential — no parallel execution
- File:
ExternalAPI/Pan/PanAPI.cs
New System NEW
- Service:
BackgroundCheckService.RunAllBackgroundChecksAsync()
- Trigger: Fired via
Task.Run() after OTP verified — non-blocking
- Step 1: Zintlr (plain mobile number)
- Step 2: Hyperverge + C-safe in parallel (
Task.WhenAll)
- Step 3: NSDL + CVL KRA in parallel (
Task.WhenAll)
- UTI Fallback: On NSDL failure (IsSuccess=false OR exception) → UTI fallback
- File:
Services/Stage2/BackgroundCheckService.cs
Key Difference: Old system ran PAN checks at Stage 4 (PAN entry), all sequentially. New system fires ALL background checks immediately after OTP verified, as a unified async pipeline with parallel steps. Customer never waits — proceeds to Stage 3 instantly.
2.4 Zintlr Phone-to-PAN Lookup
Query Zintlr with the verified mobile number to retrieve the customer's PAN. Store for pre-fill at Stage 4.
BRD / PRD Requirement PRD
BRD: Query Zintlr with the verified mobile number to retrieve PAN. Store the PAN (hashed) for pre-fill at Stage 4 PAN entry.
- Uses plain mobile number (not hashed) for the API call
- PAN hashed on return before storage
Old System OLD
- Method:
PanAPI.CallMobileToPanDetailsAsync() — called MobileToPanDetailsAPI URL
- Logging:
USP_MobileToPanLog_SJET
- File:
ExternalAPI/Pan/PanAPI.cs
New System NEW
- Provider:
ZintlrPhoneToPanProvider.LookupPanByPhoneAsync(name, plainMobile, leadId)
- Input: Uses
lead.Mobile (plain, not hash) for the API call
- Output: PAN hashed on return before storage
- File:
ExternalProviders/Pan/ZintlrPhoneToPanProvider.cs
Key Difference: Old system used a generic MobileToPanDetailsAPI URL. New system has a dedicated ZintlrPhoneToPanProvider with explicit plain-mobile input and PAN hashing on return. Provider pattern enables easy vendor swap.
2.5 Hyperverge PAN Name+DOB Fetch
If Zintlr returns a PAN, fetch the customer's name and date of birth from the Income Tax Department via Hyperverge.
BRD / PRD Requirement PRD
BRD: If Zintlr returns a PAN, fetch name + DOB from the Income Tax Department via Hyperverge API. Used for pre-fill and validation at PAN stage.
Old System OLD
- Methods:
PanAPI.GetPanDetailsHyperverge() / GetPanDetailsHypervergeAsync()
- Endpoint: POST to
HYPERVERGE_PAN_APIURL_STD
- Response:
HyperData object with name and DOB fields
- File:
ExternalAPI/Pan/PanAPI.cs
New System NEW
- Provider:
HypervergePanProvider.FetchPanDetailsAsync(panNumber, leadId)
- DOB Parsing: Handles multiple date formats from Hyperverge response
- Output: Structured name + DOB for pre-fill
- File:
ExternalProviders/Pan/HypervergePanProvider.cs
Key Difference: Old system had sync/async variants in a monolithic PanAPI class. New system uses a dedicated HypervergePanProvider with robust DOB multi-format parsing. Clean separation of vendor-specific logic.
2.6 NSDL PAN Validation (with UTI Fallback)
Validate PAN is active, belongs to an individual (4th char = P), and name+DOB match. UTI is fallback only when NSDL is down.
BRD / PRD Requirement PRD
BRD: Validate PAN is valid, individual (4th char = P), Name + DOB match. UTI fallback only when NSDL is DOWN.
- PAN status: E = active
- Name match: Y/N
- DOB match: Y/N
- Seeding status check
- UTI only when NSDL provider is unavailable
Old System OLD
- Method:
PanAPI.VerifyPAN_NSDL()
- Provider Selection:
GET_PAN_SERVICE_NAME() to choose NSDL vs UTI
- Fallback: Dual fallback between NSDL and UTI
- Response Fields: pan_status (E=active), name (Y/N), dob (Y/N), seeding_status
- File:
ExternalAPI/Pan/PanAPI.cs
New System NEW
- Method:
BackgroundCheckService.RunNsdlPanValidationAsync()
- Primary:
NsdlPanProvider.ValidatePanAsync()
- UTI Fallback Triggers:
- Result is null
- IsSuccess = false
- Exception thrown
- Both Fail: Store result as "PROVIDER_DOWN"
- File:
Services/Stage2/BackgroundCheckService.cs
Key Difference: Old system used a config-driven provider switch (GET_PAN_SERVICE_NAME) that could choose either NSDL or UTI as primary. New system always uses NSDL as primary, UTI as fallback only on failure. If both fail, explicitly stores "PROVIDER_DOWN" status.
2.7 CVL KRA Status Fetch
Fetch KRA status using PAN. Map raw CVL code to internal status. Download prefill data for later stages.
BRD / PRD Requirement PRD
BRD: Fetch KRA status with PAN. Map raw CVL code to internal status. Download prefill data (name, DOB, address, email, etc.) for use in later stages.
- KRA status determines whether KYC data is available for pre-fill
- Email from KRA used as prefill option at Stage 3
Old System OLD
- Logging SP:
USP_INSERT_KRA_LOGS_SJET — logged all KRA API calls
- Decision SP:
USP_CALL_KRAAPI_ONSUBMIT_SJET — determined if KRA API should be called
- Storage: KRA data stored in
TBL_KRAAPI_CALL_LOG
New System NEW
- Method:
BackgroundCheckService.RunCvlKraCheckAsync()
- Provider Calls:
CvlKraPanProvider.ValidatePanAsync() then DownloadKraDataAsync()
- Status Mapping: Raw CVL code mapped via
CvlStatusMapping table + hardcoded fallback dictionary
- Entity: Results stored in
KraRecord entity
- File:
Services/Stage2/BackgroundCheckService.cs
Key Difference: Old system used separate SPs for logging, decision, and storage across different tables. New system is a single method calling a dedicated provider, with status mapping via a configurable database table (with hardcoded fallback). Results stored in a clean KraRecord entity.
2.8 C-safe AML/SEBI/PEP Check
Check PAN against SEBI debarred list, AML watchlist, PEP list, and terrorism lists. FLAGGED leads become NON_STP at Stage 11 (not blocking here).
BRD / PRD Requirement PRD
BRD: Check PAN against SEBI debarred, AML, PEP, and terrorism lists via C-safe. If FLAGGED, the lead is marked NON_STP at Stage 11 — it does NOT block the journey at this stage.
- SEBI debarred check
- AML watchlist check
- PEP (Politically Exposed Person) check
- Terrorism list check
- Non-blocking at Stage 2 — affects STP decision at Stage 11
Old System OLD
- Config SP:
USP_GETCSAFEFLAG_SJET — retrieved C-safe configuration flags
- Request/Response SP:
USP_INSERT_CSAFE_Req_Resp — stored API request and response
- PEP Update SP:
USP_INSERT_UPDATE_CSAFE_PEP_SJET — updated PEP flags on TBL_OAO_DETAILS and TBL_CLIENT_WORKDETAILS
New System NEW
- Method:
BackgroundCheckService.RunCsafeCheckWithPanAsync()
- Provider:
CsafeProvider.CheckPanAsync(panHash, leadId)
- Entity:
CsafeCheck with boolean fields:
SebiDebarred
AmlFlagged
PepFlagged
TerrorismFlagged
- File:
Services/Stage2/BackgroundCheckService.cs
Key Difference: Old system used 3 separate SPs (config fetch, request/response logging, PEP flag update) and stored flags across multiple tables. New system has a single provider call with results stored in a clean CsafeCheck entity with explicit boolean fields for each check type.
2.9 Downstream Events (OTP Verified)
Publish events to CleverTap, Zoho CRM, Datalake, and CDP when OTP is verified.
BRD / PRD Requirement PRD
BRD: On OTP verification, publish events to CleverTap, Zoho CRM, Datalake, and CDP. All async and non-blocking.
Old System OLD
- CRM: Inline calls to LeadSquare + Zoho CRM (synchronous)
- Email:
USP_SEND_EMAIL_ONLOGIN_SJET — sent email notification on login
- Pattern: Direct API calls inline during OTP flow
New System NEW
- Method:
OtpVerificationService.PublishOtpVerifiedDownstreamEventsAsync()
- Pattern: Creates 4
DownstreamEvent records:
- CLEVERTAP
- ZOHO_CRM
- DATALAKE
- CDP
- Execution: Async, non-blocking — events queued in DB, processed by background worker
Key Difference: Old system made synchronous inline calls to LeadSquare + Zoho. New system publishes 4 async downstream events (no LeadSquare). CRM/analytics failures never block the OTP verification flow.
Stage 3 — Email Verification
3.1 Email Capture Decision (KRA Prefill / Google OAuth / Manual OTP)
Three paths to capture email: KRA email available (confirm, no OTP), Google OAuth (no OTP), or manual entry (4-digit OTP). Wait max 3 seconds for KRA result.
BRD / PRD Requirement PRD
BRD Stage 3 — EMAIL_VERIFIED: Three paths for email capture:
- KRA Prefill: If KRA email is available from background check → customer confirms (no OTP needed)
- Google OAuth: Customer authenticates via Google → email captured (no OTP needed)
- Manual Entry: Customer types email → 4-digit OTP sent to that email
- Wait maximum 3 seconds for KRA background check result before showing options
Old System OLD
- Method:
LoginRepository.GenerateEmailOTP() — single path only (manual OTP)
- No KRA prefill path
- No Google OAuth path
- SP:
USP_SEND_RESUME_EMAILOTP_SJET — generated OTP for email
- File:
Joruney_imp_code\Registration\LoginRepository.cs
New System NEW
- Service:
EmailVerificationService.InitiateEmailVerificationAsync()
- Routing by
EmailSource:
KRA_PREFILL → HandleKraPrefillVerificationAsync() (no OTP)
GOOGLE_OAUTH → HandleGoogleOAuthVerificationAsync() (no OTP)
MANUAL_OTP → HandleManualOtpInitiationAsync() (4-digit OTP, 10-min TTL)
- KRA Wait:
GetEmailDetailsAsync() has 3-second KRA wait via polling loop
- File:
Services/Stage3/EmailVerificationService.cs
Key Difference: Old system had only one path (manual OTP via SP). New system supports 3 paths: KRA prefill (no OTP), Google OAuth (no OTP), and manual OTP. The 3-second KRA polling wait is entirely new — enables pre-fill from background checks fired at Stage 2.
3.2 Email Format Validation (Custom Rules)
Reject forbidden patterns (notprovided, noemail, xyz), domains starting with digits, domains ending with periods, and multiple @ signs.
BRD / PRD Requirement PRD
BRD: Custom email validation rules beyond standard format check:
- Reject forbidden patterns: notprovided, noemail, xyz, etc.
- Reject domain starting with a digit
- Reject domain ending with a period
- Reject multiple @ signs
- Whitelist of allowed domains can skip format checks
Old System OLD
- SP:
USP_CUSTOM_EMAIL_VALIDATION_SJET — extensive SP-based format checks
- SP:
USP_ALLOW_EMAIL_SJET — whitelist check
- Table:
TBL_ALLOW_EMAIL — whitelist of allowed email patterns
- Logic: Domain and pattern rules implemented in SQL stored procedures
New System NEW
- Method:
EmailVerificationService.ValidateEmailFormat() (static method)
- Checks:
- ForbiddenEmailPatterns array (notprovided, noemail, xyz, etc.)
char.IsDigit(domain[0]) — domain starts with digit
endsWith('.') — domain ends with period
@ count > 1
- Whitelist:
AllowedEmailDomain table — whitelisted domains skip format checks
- Config Toggles: ENABLE_CUSTOM_FORMAT_CHECK, ENABLE_WHITELIST_CHECK, ENABLE_RESTRICTED_DOMAIN_CHECK
- File:
Services/Stage3/EmailVerificationService.cs
Key Difference: Old system ran all validation in SQL SPs. New system uses a static C# method with a configurable forbidden patterns array and feature toggles for each check type. Whitelisted domains skip all custom format checks.
3.3 Restricted Email Domain Check
Block disposable email domains (temporary/throwaway email providers).
BRD / PRD Requirement PRD
BRD: Block disposable/temporary email domains. If the email domain is on the restricted list, reject with error.
Old System OLD
- SP:
USP_RESTRICT_EMAIL_DOMAIN_SJET
- Table:
TBL_RESTRICT_EMAIL_DOMAIN
New System NEW
- Query:
_db.Set<RestrictedEmailDomain>().AnyAsync(r => r.Domain == emailDomain && r.IsActive)
- Table:
restricted_email_domains
- Error:
EMAIL_DOMAIN_RESTRICTED
Key Difference: Old system used a stored procedure to check TBL_RESTRICT_EMAIL_DOMAIN. New system queries the restricted_email_domains table directly via EF Core. Same logic, cleaner implementation.
3.4 Email Duplicate Check (Post-eSign)
If the email hash is already linked to a post-eSign lead, block the email. Bypass logic checked before blocking.
BRD / PRD Requirement PRD
BRD: Email hash already linked to a post-eSign lead → block. Check bypass eligibility before hard-blocking.
Old System OLD
- SP:
USP_SEND_RESUME_EMAILOTP_SJET — checked duplicates inline
- Tables checked:
TBL_DEDUPE_DATA_DUMP
TBL_CLIENT_CLIENTCODE
TBL_CLIENT_STAGEDETAILS
TBL_CLIENT_PERSONALDETAILS
New System NEW
- Service:
EmailVerificationService joins Leads (stage ≥ Esign) with EmailVerifications by emailHash
- Bypass: If duplicate found, calls
BackOfficeCheckService.CheckBypassEligibilityAsync("EMAIL", email) before blocking
- Error:
BE_EMAIL_DUPLICATE
Key Difference: Old system checked duplicates across 4 separate tables in a single massive SP. New system joins Leads + EmailVerifications with a stage filter (post-eSign only) and checks bypass eligibility via a separate service before blocking.
3.5 Email Bypass Logic
Same bypass pattern as mobile but for EMAIL type. Allow registration when email exists in back-office for INACTIVE, PMS-only, or OWNER accounts.
BRD / PRD Requirement PRD
BRD: Same as mobile bypass but for EMAIL type. Bypass conditions: INACTIVE accounts, PMS-only accounts, branch/subbroker OWNER accounts.
Old System OLD
- SP:
USP_BYPASS_MOBILE_EMAIL_PAN_SJET — EMAIL section
- Tables checked:
MOSL_FEED_CLIENT_DETAILS by EmailId, MOSL_FEED_BRANCH by BA_EMAIL / CORR_EMAIL
New System NEW
- Method:
BackOfficeCheckService.CheckEmailBypassAsync()
- Tables checked:
client_master — by Email
branch_master — by BaEmail / CorrEmail
subbroker_master
duplicate_bypass_whitelist
- File:
Services/Common/BackOfficeCheckService.cs
Key Difference: Old system checked MOSL_FEED tables directly in a shared SP (MOBILE/EMAIL/PAN in one SP). New system has a dedicated CheckEmailBypassAsync() method checking synced tables. Added duplicate_bypass_whitelist as an additional bypass path.
3.6 Email OTP Generation & Delivery
Generate a 4-digit OTP with 10-minute TTL for manual email entry. Max 5 wrong attempts (must change email), max 3 resends, 30-second cooldown.
BRD / PRD Requirement PRD
BRD: 4-digit OTP for manual email verification:
- 10-minute TTL
- Max 5 wrong attempts → must change email address
- Max 3 resends
- 30-second cooldown between resends
Old System OLD
- SP:
USP_SEND_RESUME_EMAILOTP_SJET — stored OTP in database
- Email Delivery:
EmailAPI.sendMail_Netcore() — sent OTP email via Netcore
- Validation (removed): CYBRIDGE/Karza/Custom SP email validation — all removed per Product team
New System NEW
- OTP Storage:
InMemoryOtpStore.Store(leadId.ToString(), "EMAIL", otp, leadId, 10min)
- Delivery:
SendEmailOtpAsync() via INotificationService
- Cooldown:
InMemoryOtpStore.Store(leadId, "EMAIL_COOLDOWN", "1", 30sec) — cooldown tracked as separate in-memory entry
- File:
Services/Stage3/EmailVerificationService.cs
Key Difference: Old system stored email OTP in database and sent via EmailAPI.sendMail_Netcore(). New system stores OTP in-memory only (10-min TTL) and uses INotificationService abstraction. Cooldown tracked as a separate in-memory entry with 30-sec TTL. CYBRIDGE/Karza email validation removed entirely.
3.7 Email OTP Verification
Verify the email OTP. On success, transition lead state to EMAIL_VERIFIED. 5 wrong attempts locks the email (must enter a different one).
BRD / PRD Requirement PRD
BRD: Verify OTP submitted by customer. On success → lead.state = EMAIL_VERIFIED. 5 wrong attempts → email locked, customer must enter a different email address.
Old System OLD
- Validation SP:
USP_VALIDATE_UPDATE_OTP_EMAIL_SJET — validated OTP from database
- Stage Update SP:
USP_UPDATE_OTPEMAIL_STAGE_SJET — updated stage in TBL_CLIENT_STAGEDETAILS
- Integration: SuperApp integration for cloud-enabled leads
New System NEW
- Method:
EmailVerificationService.VerifyEmailOtpAsync()
- OTP Retrieval:
InMemoryOtpStore.Get(leadId, "EMAIL")
- Tracking:
AttemptCount tracked in OtpVerification entity
- Lock: 5th wrong attempt →
IsLocked = true, must enter different email
- On Success: lead.state = EMAIL_VERIFIED, publishes 4 downstream events
- File:
Services/Stage3/EmailVerificationService.cs
Key Difference: Old system validated OTP from database via SP, updated stage via separate SP. New system validates from in-memory store, tracks attempts in OtpVerification entity, and locks the email (not the lead) after 5 wrong attempts — customer can try a different email. Downstream events published automatically on success.
3.8 Downstream Events (Email Verified)
Publish events to CleverTap, Zoho CRM, CDP, and Datalake when email is verified.
BRD / PRD Requirement PRD
BRD: On email verification, publish events to CleverTap, Zoho CRM, CDP, and Datalake. All async and non-blocking.
Old System OLD
- Stage Update:
USP_UPDATE_OTPEMAIL_STAGE_SJET
- CRM: LSQ opportunity update (synchronous)
- Email: Email sent on login notification
New System NEW
- Pattern: 4
DownstreamEvent records created with EventType = EMAIL_VERIFIED:
- CLEVERTAP
- ZOHO_CRM
- CDP
- DATALAKE
- Payload: Includes
EmailSource (KRA_PREFILL / GOOGLE_OAUTH / MANUAL_OTP) and KraPrefillUsed flag
- Execution: Async, non-blocking — events queued in DB, processed by background worker
Key Difference: Old system updated stage via SP and made synchronous CRM calls. New system publishes 4 async downstream events with rich payload (EmailSource, KraPrefillUsed). No LeadSquare — Zoho CRM only. Event payloads carry the email capture method for analytics.