Payment Processor Abstraction & Fiserv Migration

Executive Summary

Goal: Build a payment processor-agnostic architecture and migrate from Authorize.Net to Fiserv Commerce Hub.

Why:


Current State (Authorize.Net)

Architecture

Browser → Accept.js (tokenize) → payment_nonce → Server
Server → Authorize.Net API → Create Customer Profile
Server → Authorize.Net API → Create Payment Profile (vault card)
Server → Authorize.Net API → Authorize Payment (hold funds)
Server → Database → Save PaymentProfile + PaymentTransaction
Later → Authorize.Net API → Capture Authorization (charge card)

Key Files

Database Schema (Current)

PaymentProfile
  - authorize_customer_profile_id  # Authorize.Net customer vault ID
  - authorize_payment_profile_id   # Authorize.Net payment method token
  - card_type, last_four, expiration_month, expiration_year
  - company_id or contact_id (polymorphic)

PaymentTransaction
  - authorize_transaction_id       # Authorize.Net transaction ID
  - transaction_type               # auth_only, capture, refund
  - amount, status, response_code
  - order_id, payment_profile_id

Problems

  1. Tight coupling to Authorize.Net (field names, service class)
  2. PCI burden - SAQ A-EP compliance required
  3. Processing fees - Authorize.Net charges per transaction
  4. Vendor lock-in - Hard to migrate to different processor

Target State (Processor-Agnostic + Fiserv)

Architecture

Browser → Fiserv Hosted Fields (iframe) → sessionId → Server
Server → Fiserv Data Capture API → Tokenize payment
Server → Fiserv Payments API → Authorize Payment (hold funds)
Server → Database → Save PaymentProfile + PaymentTransaction
Later → Fiserv Payments API → Capture Authorization (charge card)

// Abstraction Layer
Controller → PaymentProcessorFactory → FiservAdapter or AuthorizeNetAdapter
Adapter → Processor-specific API calls
Adapter → Returns standardized response

New Database Schema

PaymentProfile
  - processor              # 'authorize_net', 'fiserv', 'stripe', etc.
  - processor_customer_id  # Generic customer ID in processor vault
  - processor_token_id     # Generic payment method token
  - card_type, last_four, expiration_month, expiration_year
  - company_id or contact_id (polymorphic)
  - is_default

PaymentTransaction
  - processor              # Which processor handled this transaction
  - processor_transaction_id  # Generic transaction ID
  - transaction_type       # auth_only, capture, refund
  - amount, status, response_code
  - order_id, payment_profile_id
  - metadata (jsonb)       # Processor-specific extra data

Implementation Plan

Phase 1: Create Abstraction Layer

Goal: Extract Authorize.Net into adapter pattern without breaking existing functionality

Tasks:

  1. Create base interface

  2. Create Authorize.Net adapter

  3. Create factory

  4. Update controllers

  5. Add tests

Deliverable: Same functionality, but using adapter pattern


Phase 2: Database Migration

Goal: Add processor-agnostic columns while keeping existing ones for backwards compatibility

Tasks:

  1. Generate migration for PaymentProfile

    add_column :payment_profiles, :processor, :string
    add_column :payment_profiles, :processor_customer_id, :string
    add_column :payment_profiles, :processor_token_id, :string
    add_index :payment_profiles, [:processor, :processor_customer_id]
    
  2. Generate migration for PaymentTransaction

    add_column :payment_transactions, :processor, :string
    add_column :payment_transactions, :processor_transaction_id, :string
    add_column :payment_transactions, :metadata, :jsonb
    add_index :payment_transactions, [:processor, :processor_transaction_id]
    
  3. Data migration

    # Backfill existing records
    PaymentProfile.where(processor: nil).update_all(
      processor: 'authorize_net',
      processor_customer_id: 'authorize_customer_profile_id',
      processor_token_id: 'authorize_payment_profile_id'
    )
    
  4. Update models

Deliverable: Database ready for multiple processors


Phase 3: Implement Fiserv Adapter

Goal: Build Fiserv integration alongside existing Authorize.Net

Tasks:

  1. Setup Fiserv credentials

  2. Create Fiserv adapter

  3. Create Fiserv helper

  4. Update payment view

  5. Update controller

  6. Add tests

Deliverable: Working Fiserv integration running alongside Authorize.Net


Phase 4: Migration & Cutover

Goal: Migrate existing customers and switch default processor

Tasks:

  1. Add processor selection to admin

  2. Test Fiserv in production (parallel running)

  3. Gradual migration

  4. Update defaults

  5. Monitor & optimize

Deliverable: Fiserv as primary processor, Authorize.Net as fallback


Phase 5: Cleanup (Optional)

Goal: Remove Authorize.Net if no longer needed

Tasks:

  1. Identify customers still on Authorize.Net
  2. Migrate remaining customers or keep for legacy
  3. Remove Accept.js code if all migrated
  4. Archive AuthorizeNetPaymentService if unused
  5. Remove old database columns after 6-month grace period

Technical Details

Fiserv Integration Specifics

Hosted Fields Setup

// 1. Request session credentials from backend
const response = await fetch('/api/payment/session', { method: 'POST' });
const { sessionId, publicKey } = await response.json();

// 2. Initialize Fiserv SDK
const hostedFields = await fiserv.hostedFields.create({
  sessionId: sessionId,
  publicKey: publicKey,
  fields: {
    cardNumber: { selector: '#card-number' },
    expirationDate: { selector: '#expiration' },
    cvv: { selector: '#cvv' }
  },
  styles: {
    // Custom styling to match your brand
  }
});

// 3. On submit, capture payment data
hostedFields.submit().then(result => {
  if (result.status === 'success') {
    // Submit form with sessionId to your server
    document.getElementById('payment_session_id').value = sessionId;
    form.submit();
  }
});

Backend Session Creation

# app/controllers/api/payment_controller.rb
def create_session
  # Generate session credentials
  response = Faraday.post(
    'https://prod.api.fiservapps.com/ch/payments/v1/payment-sessions',
    { amount: params[:amount], currency: 'USD' }.to_json,
    {
      'Api-Key' => ENV['FISERV_API_KEY'],
      'Authorization' => generate_hmac_signature,
      'Content-Type' => 'application/json'
    }
  )

  json = JSON.parse(response.body)
  render json: {
    sessionId: json['sessionId'],
    publicKey: json['publicKeyBase64']
  }
end

Payment Authorization

# app/services/payment_processors/fiserv_adapter.rb
def authorize_payment(amount:, session_id:, order_details:)
  response = Faraday.post(
    'https://prod.api.fiservapps.com/ch/payments/v1/charges',
    {
      amount: { total: amount, currency: 'USD' },
      source: { sourceType: 'PaymentSession', sessionId: session_id },
      transactionDetails: {
        captureFlag: false  # Authorization only
      }
    }.to_json,
    headers
  )

  # Parse response and return standardized format
  {
    success: response.status == 200,
    transaction_id: json['transactionId'],
    status: json['transactionStatus']
  }
end

Standard Interface Methods

All adapters must implement:

module PaymentProcessors
  class Base
    # Customer Management
    def create_customer(customer_data)
      # Returns: { customer_id: 'cust_123' }
    end

    # Payment Token Management
    def create_payment_token(customer_id:, payment_data:)
      # payment_data can be payment_nonce (Auth.Net) or session_id (Fiserv)
      # Returns: { token_id: 'tok_123', card_type: 'Visa', last_four: '1234' }
    end

    # Transactions
    def authorize_payment(amount:, token_id:, order_details:)
      # Returns: { transaction_id: 'txn_123', status: 'authorized', amount: 100.00 }
    end

    def capture_authorization(transaction_id:, amount:)
      # Returns: { transaction_id: 'txn_456', status: 'captured', amount: 100.00 }
    end

    def refund_transaction(transaction_id:, amount:)
      # Returns: { transaction_id: 'txn_789', status: 'refunded', amount: 100.00 }
    end

    # Helpers
    def errors
      # Returns array of error messages
    end
  end
end

Configuration

Environment Variables

# Processor Selection
DEFAULT_PAYMENT_PROCESSOR=fiserv  # or 'authorize_net'

# Authorize.Net (existing)
AUTHORIZENET_API_LOGIN_ID=xxx
AUTHORIZENET_TRANSACTION_KEY=xxx
AUTHORIZENET_PUBLIC_CLIENT_KEY=xxx

# Fiserv (new)
FISERV_API_KEY=xxx
FISERV_API_SECRET=xxx
FISERV_MERCHANT_ID=xxx
FISERV_ENVIRONMENT=prod  # or 'cert' for testing

Per-Customer Processor Selection

# Add to Company model
class Company
  def payment_processor
    # Can be set per company to override default
    settings[:payment_processor] || ENV['DEFAULT_PAYMENT_PROCESSOR']
  end
end

# Usage in controller
processor = PaymentProcessorFactory.for(@order.company.payment_processor)

Testing Strategy

Unit Tests

Integration Tests

Manual Testing Checklist


Rollback Plan

If Fiserv has issues:

  1. Change DEFAULT_PAYMENT_PROCESSOR=authorize_net in .env
  2. Restart server (will route new payments to Authorize.Net)
  3. Existing Fiserv authorizations can still be captured via adapter

No code changes needed - just config change.


Success Metrics


Timeline Estimate

Total: ~3-4 weeks for full migration


Questions/Decisions Needed

  1. Fiserv Account: Do we have Fiserv Commerce Hub credentials?
  2. Migration Date: When should we start migrating customers?
  3. Authorize.Net Retention: Keep as fallback or fully deprecate?
  4. Customer Communication: Inform customers of payment processor change?
  5. Hosted Components vs Hosted Fields: Which Fiserv integration type? (Recommend Hosted Fields for max customization)

Resources