02 - Mock Mode

Mock mode is the backend's primary dev-safety feature. One flag in appsettings controls whether all external provider calls are real or mocked. Critical for local development without hitting production providers.

Two levels of mock control

Global: MockMode.GlobalMockEnabled in appsettings.Development.json. Affects ~13 services simultaneously.
Per-provider: provider_configurations.mock_mode column in DB. Toggles a single provider live or mocked without touching appsettings.

Global Mock Mode Setup

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

How Services Read the Flag

Every service that interacts with an external provider reads MockMode:GlobalMockEnabled in its constructor and branches accordingly.

backend/src/MO.Ekyc.Infrastructure/Services/Common/BankAutoVerifyService.csC#
public class BankAutoVerifyService
{
    private readonly bool _globalMockEnabled;

    public BankAutoVerifyService(..., IConfiguration config, ...)
    {
        _globalMockEnabled = config.GetValue("MockMode:GlobalMockEnabled", false);
    }

    public async Task TriggerAutoVerifyAsync(Guid leadId, string mobileNumber, ...)
    {
        if (_globalMockEnabled)
        {
            _logger.LogInformation("Lead {LeadId}: Bank auto-verify MOCKED", leadId);
            // Persist mock record with fake HDFC active savings data
            return;
        }
        // ... real Decentro call ...
    }
}

Services Reading GlobalMockEnabled

StageServiceMocked behavior
2 (background)BankAutoVerifyServiceDecentro returns mock HDFC active savings account
2 (background)BackgroundCheckServiceZintlr and CSafe skipped
0FraudCheckServiceIP velocity, device fingerprint always pass
4PanVerificationServiceReturns mock success with fake name
5AadhaarVerificationServiceReturns mock redirect URL containing example.com
6BankVerificationServiceRPD returns mock redirect URL
7LivenessServiceSkips camera/SDK, returns success with mock score
8SignatureServiceAccepts any non-empty signature
12DocumentGenerationServiceReturns mock PDF URL instead of iText7 generation
13EsignServiceReturns mock eSign redirect URL
14AccountCreationServiceSkips CBOS call, returns mock clientId
15FundTransferServiceReturns test Razorpay key + mock redirect URL

Per-Provider Mock Mode

Beyond the global flag, each external provider has its own mock_mode column in provider_configurations. The ProviderChainExecutor<T> reads this per request - no restart needed.

SQLView and toggle per-provider mock
-- View all providers and their mock state
SELECT provider_name, category, priority, enabled, mock_mode
FROM ekyc.provider_configurations
ORDER BY category, priority;

-- Force HyperVerge liveness live while everything else stays mock
UPDATE ekyc.provider_configurations
SET mock_mode = false
WHERE provider_name = 'HypervergeLiveness';
backend/src/MO.Ekyc.Infrastructure/ExternalProviders/Common/ProviderChainExecutor.csC#mock check
foreach (var config in enabledProviders)
{
    if (config.MockMode)
    {
        _logger.LogInformation("Provider {Name} is in MOCK mode, skipping real call", config.ProviderName);
        await _auditLogger.LogAsync(..., isMock: true);
        return default!;
    }

    try
    {
        return await operation(provider);
    }
    catch (Exception ex) { ... }
}

SMS & Email Fixed OTP

OTP providers are separately controlled by Sms.Mode and Email.Mode. In FIXED_OTP mode, the backend stores and accepts 1234 for all OTP verifications.

backend/src/MO.Ekyc.Infrastructure/ExternalProviders/NetcoreSmsProvider.csC#mode check
var mode = _configuration["Sms:Mode"] ?? "REAL";

if (mode == "FIXED_OTP")
{
    var fixedOtp = _configuration["Sms:FixedOtp"] ?? "1234";
    _logger.LogInformation("SMS MOCKED - returning fixed OTP {FixedOtp}", fixedOtp);
    return new SmsResult { Success = true, Otp = fixedOtp };
}

// ... real Netcore call ...

Troubleshooting

OTP 1234 is rejected

Real HTTP call happens in mock mode

Frontend shows real WebView in mock mode

The Flutter app detects mock URLs (example.com, mock-digilocker) and bypasses WebViews. If this isn't working: