02 - Mock Mode

Mock mode is the reason you can run the entire 16-stage eKYC journey on your laptop without real SMS, NSDL calls, or bank verification. Understanding it is critical for local development.

📊 Key fact: Mock mode is backend-driven

The Flutter app has no mock flag. Mock mode lives entirely in the backend's appsettings.Development.json. When enabled, the backend returns mock data (fake URLs, fixed OTPs, simulated provider responses) and the Flutter app just reacts to what the backend sends.

Backend Setup

Mock mode is controlled by a single flag in the backend's Development settings file.

backend/src/MO.Ekyc.Api/appsettings.Development.jsonJSON
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft.AspNetCore": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  },
  "ConnectionStrings": {
    "EkycDb": "Host=127.0.0.1;Port=5432;Database=ekyc3;Username=postgres;Password=root123;..."
  },
  "MockMode": {
    "GlobalMockEnabled": true
  },
  "Payment": {
    "Razorpay": {
      "ClientApiKey": "rzp_test_placeholder"
    }
  },
  "Sms": {
    "Mode": "FIXED_OTP",
    "FixedOtp": "1234"
  },
  "Email": {
    "Mode": "FIXED_OTP",
    "FixedOtp": "1234"
  }
}
Three independent switches

1. GlobalMockEnabled - affects all backend services. When true, they skip external provider calls and return mock data.
2. Sms.Mode: FIXED_OTP - uses 1234 for all mobile OTPs.
3. Email.Mode: FIXED_OTP - uses 1234 for all email OTPs.

Where the Backend Reads This Flag

At least 13 backend services check GlobalMockEnabled and change behavior accordingly.

ServiceStageWhat it mocks
BankAutoVerifyService.cs2 (post-OTP)Decentro mobile-to-bank: returns mock HDFC active savings
PanVerificationService.cs4NSDL PAN verify: returns mock success with fake name
AadhaarVerificationService.cs5DigiLocker: returns mock redirect URL (example.com/mock-digilocker)
BankVerificationService.cs6Hyperverge RPD: returns mock redirect URL
LivenessService.cs7HyperVerge liveness: skips SDK call, returns success
SignatureService.cs8AINxt signature validation: bypassed
DocumentGenerationService.cs12AOF PDF generation: returns mock PDF URL
EsignService.cs13eMudhra/HyperVerge eSign: returns mock redirect URL
AccountCreationService.cs14CBOS account creation: returns mock clientId
FundTransferService.cs15Razorpay: returns test key + mock redirect
BackgroundCheckService.cs2 (async)Zintlr + C-SAFE: skipped
FraudCheckService.cs0IP velocity, device fingerprint: always pass
WarRoomService.csAdminProvider health: returns mock metrics
Backend source files
  • backend/src/MO.Ekyc.Api/appsettings.Development.json
  • backend/src/MO.Ekyc.Infrastructure/Services/Common/BankAutoVerifyService.cs
  • backend/src/MO.Ekyc.Infrastructure/Services/Stage2/OtpVerificationService.cs
  • backend/src/MO.Ekyc.Infrastructure/ExternalProviders/Common/ProviderChainExecutor.cs
  • Backend docs (pending) - placeholder

Per-Provider Mock Mode

Beyond the global switch, each external provider has its own mock_mode flag in the provider_configurations database table. This lets you force one provider live while keeping others mocked.

SQLToggle a single provider to live
-- See current provider mock settings
SELECT provider_name, category, mock_mode
FROM ekyc.provider_configurations
ORDER BY category, priority;

-- Turn off mock for HyperVerge liveness (go live for this one provider)
UPDATE ekyc.provider_configurations
SET mock_mode = false
WHERE provider_name = 'HypervergeLiveness';

The backend's ProviderChainExecutor reads this per request - no restart needed.

Flutter Side: Mock URL Detection

When the backend is in mock mode, it returns fake redirect URLs containing strings like example.com, mock-digilocker, or hyperverge.co. The Flutter app detects these URLs and skips opening the WebView or UPI app, calling the success callback directly instead.

Critically, this detection is only active in dev builds. In prod builds, ApiConfig.isDev is a compile-time constant false, so the mock-bypass code is dead and tree-shaken out of the binary.

flutter_app/lib/screens/stage5_digilocker.dartDart~line 120
if (_redirectUrl != null && _redirectUrl!.isNotEmpty) {
  // Dev only: skip WebView for mock URLs from backend
  if (ApiConfig.isDev &&
      (_redirectUrl!.contains('mock-digilocker') ||
       _redirectUrl!.contains('example.com'))) {
    await _handleMockDigilocker();
    return;
  }
  _initWebView(_redirectUrl!);
  setState(() {
    _screenState = _ScreenState.webview;
    _isLoading = false;
  });
}
flutter_app/lib/screens/stage6_bank.dartDart~line 180
// Dev only: skip external UPI launch for mock URLs from backend
if (ApiConfig.isDev &&
    (redirectUrl.contains('mock') ||
     redirectUrl.contains('example.com') ||
     redirectUrl.contains('hyperverge.co') ||
     redirectUrl.isEmpty)) {
  // Mock: skip UPI app but still show verified state for income selection
  final mockResult = await api.completeReversePenny(
    transactionRef: transactionId,
    accountNumber: '',
    ifscCode: '',
  );
  ...
}

What Behaves Differently in Mock Mode

StageLive behaviorMock behavior
2 (OTP)Real SMS with 6-digit OTPOTP is always 1234
3 (Email)Real SMTP email with OTPOTP is always 1234
4 (PAN)NSDL verify callReturns mock success
5 (DigiLocker)Real DigiLocker WebViewWebView skipped, auto-success
6 (Bank)UPI Reverse Penny Drop via UPI appUPI launch skipped, mock bank details returned. If Decentro mobile-to-bank found account, user goes directly to verified screen.
7 (Liveness)HyperVerge camera + face matchReturns success with mock score
8 (Signature)AINxt signature validationAccepts any non-empty signature
12 (Doc gen)Real iText7 PDF generationReturns mock PDF URL
13 (eSign)eMudhra / HyperVerge WebViewWebView skipped, auto-signed
14 (Account)CBOS API creates real accountReturns mock clientId
15 (Fund)Razorpay live checkoutRazorpay test key, no real charge

To see the full stage-by-stage flow for both modes, open the Old vs New UI Comparison or check the Journey Flow Bar on the WarRoom dashboard.

Testing Journey Variations

Test a specific provider live

Keep GlobalMockEnabled: true, but update the DB for the one provider you want live:

SQLForce HyperVerge live
UPDATE ekyc.provider_configurations
SET mock_mode = false
WHERE provider_name = 'HypervergeLiveness';

Test full live mode locally

Set GlobalMockEnabled: false and provide real API keys in appsettings.Development.json. This is risky - you'll hit real SMS, real NSDL, real providers. Only do this if you have non-production credentials.

Test Decentro auto-verify

Decentro mobile-to-bank is triggered at Stage 2 (OTP verified). In mock mode, it stores a fake HDFC savings account for the lead. When the user reaches Stage 6, the backend auto-promotes this to a verified bank account and the user lands directly on the Bank Verified screen. Clicking "Change bank" takes them to manual entry.

Decentro flow in backend
  • Trigger: backend/src/MO.Ekyc.Infrastructure/Services/Stage2/OtpVerificationService.cs (TriggerBankAutoVerifyIfFirstTime)
  • Service: backend/src/MO.Ekyc.Infrastructure/Services/Common/BankAutoVerifyService.cs
  • Auto-promote: backend/src/MO.Ekyc.Infrastructure/Services/Stage6/BankVerificationService.cs (GetBankDetailsAsync)

Troubleshooting

OTP 1234 is rejected

Real DigiLocker WebView opens in mock mode

Stage 6 shows hardcoded test bank in prod

This should not happen

The mock URL bypass is guarded by ApiConfig.isDev, which is a compile-time constant. In a prod build (--dart-define=ENV=prod), isDev is always false and the bypass code is dead. If you see mock bank data in prod, either the wrong build flag was used, or the guard was removed. Check env config.

Backend started but app still says "No internet"