UnlockOS Developers
← Back to blog
🛡️

Robust Error Handling in Financial Deposit Processing

Jun 8, 2026Jun 14, 2026
6 min
4 commits
Depth 7/10
error-handlingtestingtypescriptreliability

Robust Error Handling in Financial Deposit Processing

Introduction

In security-critical systems like smart lock management, handling financial transactions requires bulletproof error handling and comprehensive testing. Recent improvements to deposit processing demonstrate how proper error isolation and behavioral testing can prevent cascading failures that could compromise property access.

The Challenge: Deposit Processing in Access Control

When guests check into a property, deposit authorization must complete successfully before granting lock access. A single failure in this flow could either:

  • Block legitimate access (availability impact)
  • Grant unauthorized access (security impact)

This makes deposit processing a critical security boundary that demands robust error handling.

Extracting Complex Logic into Testable Helpers

Complex business logic embedded in larger workflows becomes difficult to test comprehensively. The solution is extraction into focused, behaviorally-tested helpers:

interface DepositGatingResult {
  success: boolean;
  errorCode?: string;
  retryable: boolean;
  auditTrail: string[];
}

class DepositGatingService {
  async validateDepositRequirement(
    booking: BookingDetails,
    paymentMethod: PaymentMethod
  ): Promise<DepositGatingResult> {
    const auditTrail: string[] = [];
    
    try {
      // Pre-authorization validation
      const preAuthResult = await this.preAuthorizeDeposit(
        paymentMethod, 
        booking.depositAmount
      );
      
      auditTrail.push(`Pre-auth: ${preAuthResult.status}`);
      
      if (!preAuthResult.success) {
        return {
          success: false,
          errorCode: preAuthResult.errorCode,
          retryable: this.isRetryableError(preAuthResult.errorCode),
          auditTrail
        };
      }
      
      return {
        success: true,
        retryable: false,
        auditTrail
      };
    } catch (error) {
      auditTrail.push(`Exception: ${error.message}`);
      
      return {
        success: false,
        errorCode: 'INTERNAL_ERROR',
        retryable: true,
        auditTrail
      };
    }
  }
  
  private isRetryableError(errorCode: string): boolean {
    const retryableCodes = [
      'NETWORK_TIMEOUT',
      'SERVICE_UNAVAILABLE',
      'RATE_LIMIT_EXCEEDED'
    ];
    return retryableCodes.includes(errorCode);
  }
}

Comprehensive Behavioral Testing

Behavioral testing focuses on outcomes rather than implementation details, making tests more resilient to refactoring:

describe('DepositGatingService', () => {
  let service: DepositGatingService;
  let mockPaymentProvider: jest.Mocked<PaymentProvider>;
  
  beforeEach(() => {
    mockPaymentProvider = createMockPaymentProvider();
    service = new DepositGatingService(mockPaymentProvider);
  });
  
  describe('when payment provider is unavailable', () => {
    it('should return retryable failure with audit trail', async () => {
      mockPaymentProvider.preAuthorize.mockRejectedValue(
        new Error('Service unavailable')
      );
      
      const result = await service.validateDepositRequirement(
        mockBooking,
        mockPaymentMethod
      );
      
      expect(result).toMatchObject({
        success: false,
        errorCode: 'INTERNAL_ERROR',
        retryable: true
      });
      
      expect(result.auditTrail).toContain(
        'Exception: Service unavailable'
      );
    });
  });
  
  describe('when insufficient funds', () => {
    it('should return non-retryable failure', async () => {
      mockPaymentProvider.preAuthorize.mockResolvedValue({
        success: false,
        errorCode: 'INSUFFICIENT_FUNDS'
      });
      
      const result = await service.validateDepositRequirement(
        mockBooking,
        mockPaymentMethod
      );
      
      expect(result.retryable).toBe(false);
    });
  });
});

Strategic Error Logging for Debugging

Proper error logging helps operations teams quickly identify and resolve issues without exposing sensitive data:

class CheckinOrchestrator {
  async processCheckin(booking: BookingDetails): Promise<CheckinResult> {
    try {
      const depositResult = await this.depositService.validateDepositRequirement(
        booking,
        booking.paymentMethod
      );
      
      if (!depositResult.success) {
        // Log failure with context but not sensitive data
        this.logger.error('Deposit validation failed during checkin', {
          bookingId: booking.id,
          errorCode: depositResult.errorCode,
          retryable: depositResult.retryable,
          auditTrail: depositResult.auditTrail,
          // Exclude sensitive payment method details
        });
        
        return {
          success: false,
          stage: 'DEPOSIT_VALIDATION',
          userMessage: this.getUserFriendlyMessage(depositResult.errorCode),
          retryable: depositResult.retryable
        };
      }
      
      // Continue with lock access provisioning
      return await this.provisionLockAccess(booking);
      
    } catch (error) {
      this.logger.error('Unexpected error in checkin process', {
        bookingId: booking.id,
        error: error.message,
        stack: error.stack
      });
      
      return {
        success: false,
        stage: 'UNKNOWN',
        userMessage: 'Please contact support',
        retryable: true
      };
    }
  }
}

Error Recovery Strategies

Different error types require different recovery strategies:

interface ErrorRecoveryStrategy {
  maxRetries: number;
  backoffMs: number;
  fallbackAction?: () => Promise<void>;
}

class ResilientDepositProcessor {
  private recoveryStrategies: Map<string, ErrorRecoveryStrategy> = new Map([
    ['NETWORK_TIMEOUT', { maxRetries: 3, backoffMs: 1000 }],
    ['RATE_LIMIT_EXCEEDED', { maxRetries: 2, backoffMs: 5000 }],
    ['INSUFFICIENT_FUNDS', { 
      maxRetries: 0, 
      backoffMs: 0,
      fallbackAction: () => this.notifyGuestOfPaymentIssue()
    }]
  ]);
  
  async processWithRecovery(
    booking: BookingDetails
  ): Promise<DepositGatingResult> {
    let attempt = 0;
    
    while (attempt < 3) {
      const result = await this.depositService.validateDepositRequirement(
        booking,
        booking.paymentMethod
      );
      
      if (result.success) {
        return result;
      }
      
      const strategy = this.recoveryStrategies.get(result.errorCode!);
      
      if (!strategy || !result.retryable || attempt >= strategy.maxRetries) {
        if (strategy?.fallbackAction) {
          await strategy.fallbackAction();
        }
        return result;
      }
      
      await this.sleep(strategy.backoffMs * Math.pow(2, attempt));
      attempt++;
    }
    
    throw new Error('Max retry attempts exceeded');
  }
}

OAuth Security Documentation

When integrating with external services like Google Calendar, transparent security documentation builds user trust:

// Required OAuth scopes with clear justification
const REQUIRED_SCOPES = {
  'https://www.googleapis.com/auth/calendar.readonly': {
    purpose: 'Read guest calendar events to prevent booking conflicts',
    dataAccessed: 'Event titles, times, and availability status',
    retention: 'Not stored - used only for real-time availability checks'
  },
  'https://www.googleapis.com/auth/calendar.calendars.readonly': {
    purpose: 'List available calendars for booking integration',
    dataAccessed: 'Calendar names and IDs',
    retention: 'Cached for 24 hours to improve performance'
  }
} as const;

Summary

Robust error handling in security-critical systems requires:

  1. Isolation of complex logic into focused, testable components
  2. Behavioral testing that validates outcomes across all error scenarios
  3. Strategic logging that aids debugging without exposing sensitive data
  4. Differentiated recovery strategies based on error type and impact
  5. Transparent security documentation for external integrations

By treating error handling as a first-class design concern, we build systems that fail gracefully and maintain security boundaries even under adverse conditions.

Key Insights

1
Error Handling

Behavioral Testing for Financial Flows

Testing deposit processing outcomes rather than implementation details creates more resilient test suites

2
Reliability

Differentiated Recovery Strategies

Different error types require specific retry policies and fallback actions to maintain system availability

3
Security

Secure Error Logging

Comprehensive audit trails that exclude sensitive data enable effective debugging while maintaining privacy

4
Testing

Extracting Critical Business Logic

Isolating complex financial validation logic into focused helpers improves testability and reliability