How we transformed fragmented payment tracking into a comprehensive admin interface that gives business teams complete visibility into every payment attempt.
This post follows the part 1 of stripe implementation we have seen. Stripe Payment – Part 1
Testing Strategy: Comprehensive Payment Testing
Payment systems are mission-critical components that directly impact revenue and customer trust, making comprehensive testing absolutely essential. A robust testing strategy must cover three distinct layers: isolated unit tests that verify individual payment service behaviours, integration tests that ensure proper webhook handling and external API interactions, and feature tests that validate the complete user experience from payment initiation to admin dashboard visibility. This multi-layered approach ensures that payment failures are caught early in development, edge cases are properly handled, and business stakeholders can rely on accurate payment data for decision-making.
Unit Testing Payment Service
Unit tests form the foundation of payment system reliability by isolating and verifying the core payment processing logic without external dependencies, ensuring that different payment scenarios (success, card declined, network errors) are handled correctly and consistently.
# spec/services/payment_service_spec.rb
RSpec.describe PaymentService do
let(:customer) { create(:customer, :with_payment_method) }
describe '.charge' do
context 'successful payment' do
before do
allow(Stripe::PaymentIntent).to receive(:create)
.and_return(double(status: 'succeeded', id: 'pi_success_123', to_hash: {}))
end
it 'creates successful transaction' do
transaction = PaymentService.charge(2999, 'Test charge', customer)
expect(transaction).to be_persisted
expect(transaction.success?).to be true
expect(transaction.amount_cents).to eq(2999)
end
it 'creates payment record association' do
expect {
transaction = PaymentService.charge(2999, 'Test charge', customer)
customer.payment_records.create!(transaction: transaction)
}.to change { customer.payment_records.count }.by(1)
end
end
context 'card declined' do
let(:declined_error) do
Stripe::CardError.new('Card declined', 'card_declined',
json_body: { 'error' => { 'code' => 'card_declined',
'message' => 'Your card was declined.' } })
end
before do
allow(Stripe::PaymentIntent).to receive(:create).and_raise(declined_error)
end
it 'creates failed transaction with error details' do
transaction = PaymentService.charge(2999, 'Test charge', customer)
expect(transaction).to be_persisted
expect(transaction.success?).to be false
expect(transaction.error_code).to eq('card_declined')
expect(transaction.error_message).to eq('Your card was declined.')
end
end
context 'network error' do
before do
allow(Stripe::PaymentIntent).to receive(:create)
.and_raise(Stripe::APIConnectionError.new('Network error'))
end
it 'creates failed transaction with network error' do
transaction = PaymentService.charge(2999, 'Test charge', customer)
expect(transaction).to be_persisted
expect(transaction.success?).to be false
expect(transaction.error_message).to eq('Network error')
end
end
context 'zero amount' do
it 'creates successful zero-amount transaction' do
transaction = PaymentService.charge(0, 'Free item', customer)
expect(transaction).to be_persisted
expect(transaction.success?).to be true
expect(transaction.amount_cents).to eq(0)
end
end
end
end
Integration Testing with Webhooks
Integration tests validate the critical communication pathways between your application and Stripe’s web-hook system, ensuring that payment status updates are properly received, parsed, and stored even when network conditions or timing issues occur.
# spec/controllers/webhooks/stripe_controller_spec.rb
RSpec.describe Webhooks::StripeController do
let(:customer) { create(:customer) }
describe 'payment_intent.payment_failed webhook' do
let(:webhook_payload) do
{
type: 'payment_intent.payment_failed',
data: {
object: {
id: 'pi_failed_123',
amount: 2999,
currency: 'usd',
customer: customer.stripe_customer_id,
last_payment_error: {
code: 'card_declined',
message: 'Your card was declined.'
}
}
}
}
end
it 'creates failed transaction record' do
expect {
post :handle_webhook, params: webhook_payload
}.to change { Transaction.count }.by(1)
transaction = Transaction.last
expect(transaction.success?).to be false
expect(transaction.error_code).to eq('card_declined')
end
it 'associates transaction with customer' do
expect {
post :handle_webhook, params: webhook_payload
}.to change { customer.payment_records.count }.by(1)
end
end
end
Feature Testing Admin Interface
Feature tests provide end-to-end validation of the admin dashboard experience, verifying that business users can access complete payment information, understand transaction statuses at a glance, and take appropriate actions based on payment data.
# spec/features/admin/customer_payments_spec.rb
RSpec.describe 'Customer Payment Admin', type: :feature do
let(:admin_user) { create(:admin_user) }
let(:customer) { create(:customer) }
before { login_as(admin_user) }
scenario 'viewing customer payment history' do
# Create test transactions
successful_transaction = create(:transaction, :successful, amount_cents: 2999)
failed_transaction = create(:transaction, :failed, amount_cents: 4999)
customer.payment_records.create!(transaction: successful_transaction)
customer.payment_records.create!(transaction: failed_transaction)
visit admin_customer_path(customer)
within('#payment-history') do
expect(page).to have_content('$29.99')
expect(page).to have_content('SUCCESS')
expect(page).to have_content('$49.99')
expect(page).to have_content('FAILED')
expect(page).to have_link('View Stripe Dashboard')
expect(page).to have_link('Retry Payment')
end
end
end
Advanced Implementation Patterns
Beyond basic payment processing, production payment systems require sophisticated patterns to handle complex business scenarios like multi-payment methods per customer, subscription lifecycle events, and intelligent error recovery. These advanced patterns separate robust enterprise systems from simple payment integrations by providing the flexibility and resilience needed for real-world business operations. Implementing these patterns proactively prevents technical debt and ensures your payment system can evolve with changing business requirements.
1. Payment Method Management System
A comprehensive payment method management system allows customers to store multiple payment methods securely while giving businesses the flexibility to handle payment method updates, expirations, and customer preferences without disrupting service continuity.
# app/services/payment_method_manager.rb
class PaymentMethodManager
def initialize(customer)
@customer = customer
end
def add_payment_method(payment_method_id)
begin
# Attach to customer
Stripe::PaymentMethod.attach(payment_method_id, {
customer: @customer.stripe_customer_id
})
# Store locally
@customer.customer_payment_methods.create!(
stripe_payment_method_id: payment_method_id,
is_default: @customer.customer_payment_methods.empty?
)
{ success: true }
rescue Stripe::InvalidRequestError => e
{ success: false, error: e.message }
end
end
def set_default_payment_method(payment_method_id)
# Update Stripe customer
Stripe::Customer.update(@customer.stripe_customer_id, {
invoice_settings: { default_payment_method: payment_method_id }
})
# Update local records
@customer.customer_payment_methods.update_all(is_default: false)
@customer.customer_payment_methods
.find_by(stripe_payment_method_id: payment_method_id)
&.update!(is_default: true)
end
def remove_payment_method(payment_method_id)
# Detach from Stripe
Stripe::PaymentMethod.detach(payment_method_id)
# Remove local record
@customer.customer_payment_methods
.find_by(stripe_payment_method_id: payment_method_id)
&.destroy!
end
end
2. Subscription Lifecycle Management
Subscription lifecycle management encompasses the complete journey from trial creation through renewal, pause, and cancellation, ensuring that billing events are properly tracked and business logic is consistently applied across all subscription state changes.
# app/services/subscription_manager.rb
class SubscriptionManager
def initialize(customer)
@customer = customer
end
def create_subscription(price_id, trial_days = nil)
subscription_params = {
customer: @customer.stripe_customer_id,
items: [{ price: price_id }],
payment_behavior: 'default_incomplete',
payment_settings: { save_default_payment_method: 'on_subscription' },
expand: ['latest_invoice.payment_intent']
}
subscription_params[:trial_period_days] = trial_days if trial_days
stripe_subscription = Stripe::Subscription.create(subscription_params)
# Create local subscription record
subscription = @customer.subscriptions.create!(
stripe_subscription_id: stripe_subscription.id,
status: stripe_subscription.status,
current_period_start: Time.at(stripe_subscription.current_period_start),
current_period_end: Time.at(stripe_subscription.current_period_end),
trial_end: stripe_subscription.trial_end ? Time.at(stripe_subscription.trial_end) : nil
)
# Track the creation attempt
if stripe_subscription.latest_invoice.payment_intent
track_subscription_payment(stripe_subscription, subscription)
end
subscription
end
private
def track_subscription_payment(stripe_subscription, local_subscription)
payment_intent = stripe_subscription.latest_invoice.payment_intent
transaction = Transaction.create!(
amount_cents: payment_intent.amount,
success: payment_intent.status == 'succeeded',
stripe_data: payment_intent.to_hash,
stripe_payment_id: payment_intent.id,
transaction_type: 'subscription_payment'
)
local_subscription.payment_records.create!(transaction: transaction)
end
end
3. Comprehensive Error Handling and Notifications
Advanced error handling goes beyond simple retry logic to include intelligent categorization of payment failures, automated customer communication workflows, and escalation procedures that help recover revenue while maintaining positive customer relationships.
# app/jobs/payment_failure_handler_job.rb
class PaymentFailureHandlerJob < ApplicationJob
def perform(transaction_id)
transaction = Transaction.find(transaction_id)
return if transaction.success?
# Find associated customer
customer = find_customer_for_transaction(transaction)
return unless customer
case transaction.error_code
when 'card_declined', 'insufficient_funds'
handle_declined_card(customer, transaction)
when 'expired_card'
handle_expired_card(customer, transaction)
when 'authentication_required'
handle_3ds_required(customer, transaction)
else
handle_generic_failure(customer, transaction)
end
end
private
def handle_declined_card(customer, transaction)
# Send customer notification
PaymentFailureMailer.card_declined(customer, transaction).deliver_now
# Update customer status
customer.update!(payment_status: 'payment_failed', last_payment_failure_at: Time.current)
# Schedule retry in 3 days
PaymentRetryJob.set(wait: 3.days).perform_later(customer.id, transaction.id)
end
def handle_expired_card(customer, transaction)
PaymentFailureMailer.card_expired(customer, transaction).deliver_now
customer.update!(payment_status: 'card_expired')
end
def find_customer_for_transaction(transaction)
payment_record = PaymentRecord.find_by(transaction: transaction)
return nil unless payment_record&.payable_type == 'Customer'
payment_record.payable
end
end
Business Intelligence and Reporting
Raw payment data becomes truly valuable when transformed into actionable business insights that drive strategic decisions and operational improvements. Business intelligence for payment systems encompasses both real-time monitoring capabilities that help identify and resolve issues quickly, and analytical reporting that reveals patterns in customer behaviour, payment success rates, and revenue optimization opportunities. These capabilities transform payment systems from cost centers into strategic business assets that actively contribute to growth and customer satisfaction.
1. Payment Analytics Dashboard
A comprehensive analytics dashboard transforms scattered payment data into clear, actionable insights that help business teams identify trends, optimize conversion rates, and proactively address payment issues before they impact revenue or customer experience.
# app/services/payment_analytics_service.rb
class PaymentAnalyticsService
def self.daily_payment_metrics(date = Date.current)
transactions = Transaction.where(created_at: date.beginning_of_day..date.end_of_day)
{
total_attempts: transactions.count,
successful_payments: transactions.successful.count,
failed_payments: transactions.failed.count,
success_rate: calculate_success_rate(transactions),
total_volume: transactions.successful.sum(:amount_cents),
average_transaction: calculate_average_amount(transactions.successful),
top_failure_reasons: top_failure_reasons(transactions.failed),
decline_by_card_type: decline_breakdown_by_card(transactions.failed)
}
end
def self.customer_payment_health(customer)
recent_transactions = customer.transactions
.where(created_at: 30.days.ago..Time.current)
.order(created_at: :desc)
{
total_transactions: recent_transactions.count,
success_rate: calculate_success_rate(recent_transactions),
consecutive_failures: calculate_consecutive_failures(recent_transactions),
days_since_last_success: days_since_last_success(recent_transactions),
risk_score: calculate_risk_score(recent_transactions)
}
end
private
def self.calculate_success_rate(transactions)
return 0 if transactions.empty?
(transactions.successful.count.to_f / transactions.count * 100).round(2)
end
def self.top_failure_reasons(failed_transactions)
failed_transactions.group(:error_code)
.count
.sort_by { |_, count| -count }
.first(5)
.to_h
end
end
2. Automated Payment Recovery
Automated payment recovery systems intelligently retry failed payments based on error type and customer history, implementing business rules that maximize revenue recovery while respecting customer preferences and avoiding negative experiences that could damage relationships.
# app/services/payment_recovery_service.rb
class PaymentRecoveryService
def self.process_failed_payments
# Find customers with recent payment failures
failed_payment_records = PaymentRecord.joins(:transaction)
.where(transactions: { success: false })
.where(created_at: 1.day.ago..Time.current)
.includes(:payable, :transaction)
failed_payment_records.each do |payment_record|
next unless payment_record.payable_type == 'Customer'
customer = payment_record.payable
retry_payment_for_customer(customer, payment_record.transaction)
end
end
private
def self.retry_payment_for_customer(customer, original_transaction)
# Only retry certain error types
return unless retryable_error?(original_transaction.error_code)
# Don't retry if customer has been marked as do-not-retry
return if customer.payment_retry_disabled?
# Attempt payment with same amount
new_transaction = PaymentService.charge(
original_transaction.amount_cents,
"Retry: #{original_transaction.stripe_data['description']}",
customer
)
customer.payment_records.create!(
transaction: new_transaction,
retry_of_transaction_id: original_transaction.id
)
if new_transaction.success?
PaymentRecoveryMailer.payment_recovered(customer, new_transaction).deliver_later
else
# Mark for manual review after multiple failures
customer.update!(requires_manual_payment_review: true)
end
end
def self.retryable_error?(error_code)
%w[api_connection_error rate_limit_error temporary_failure].include?(error_code)
end
end
Conclusion
The key principles to remember:
- Track Everything: Every payment attempt, successful or failed, tells part of your business story
- Design for Non-Technical Users: Transform complex payment data into actionable business intelligence
- Plan for Scale: Use caching, efficient queries, and smart data structures
- Test Thoroughly: Payment systems require comprehensive testing of both happy and sad paths
- Monitor Continuously: Build dashboards and alerts that help you catch issues before they impact customers
Ready to implement robust payment tracking in your Rails application? Start with the foundational data models, then build up your service layer and admin interfaces systematically. Remember: comprehensive payment visibility is not just a technical requirement—it’s a business advantage.