Goal: Build a payment processor-agnostic architecture and migrate from Authorize.Net to Fiserv Commerce Hub.
Why:
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)
app/views/app/quotes/payment.html.erb - Accept.js client integrationapp/services/authorize_net_payment_service.rb - Authorize.Net API wrapperapp/models/payment_profile.rb - Stores customer payment methodsapp/models/payment_transaction.rb - Stores transaction historyapp/controllers/app/quotes_controller.rb - Payment flow orchestrationPaymentProfile
- 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
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
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
Goal: Extract Authorize.Net into adapter pattern without breaking existing functionality
Tasks:
Create base interface
app/services/payment_processors/base.rbcreate_customer, tokenize_payment, authorize, capture, refundCreate Authorize.Net adapter
app/services/payment_processors/authorize_net_adapter.rbAuthorizeNetPaymentService logicCreate factory
app/services/payment_processor_factory.rbUpdate controllers
AuthorizeNetPaymentServicePaymentProcessorFactory.defaultAdd tests
Deliverable: Same functionality, but using adapter pattern
Goal: Add processor-agnostic columns while keeping existing ones for backwards compatibility
Tasks:
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]
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]
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'
)
Update models
Deliverable: Database ready for multiple processors
Goal: Build Fiserv integration alongside existing Authorize.Net
Tasks:
Setup Fiserv credentials
.env: FISERV_API_KEY, FISERV_API_SECRET, FISERV_MERCHANT_IDconfig/initializers/fiserv.rbCreate Fiserv adapter
app/services/payment_processors/fiserv_adapter.rbCreate Fiserv helper
app/helpers/fiserv_helper.rbUpdate payment view
Update controller
Add tests
Deliverable: Working Fiserv integration running alongside Authorize.Net
Goal: Migrate existing customers and switch default processor
Tasks:
Add processor selection to admin
Test Fiserv in production (parallel running)
Gradual migration
Update defaults
DEFAULT_PAYMENT_PROCESSOR=fiserv in production .envMonitor & optimize
Deliverable: Fiserv as primary processor, Authorize.Net as fallback
Goal: Remove Authorize.Net if no longer needed
Tasks:
AuthorizeNetPaymentService if unused// 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();
}
});
# 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
# 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
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
# 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
# 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)
If Fiserv has issues:
DEFAULT_PAYMENT_PROCESSOR=authorize_net in .envNo code changes needed - just config change.
Total: ~3-4 weeks for full migration