Skip to content

Instantly share code, notes, and snippets.

@dpmccabe
Last active December 26, 2015 23:59
Show Gist options
  • Save dpmccabe/7234858 to your computer and use it in GitHub Desktop.
Save dpmccabe/7234858 to your computer and use it in GitHub Desktop.
# == Schema Information
#
# Table name: billings
#
# id :integer not null, primary key
# user_id :integer
# bt_cc_token :string(255)
# bt_cust_id :string(255)
# bt_cc_expiration :string(255)
# bt_cc_last4 :string(255)
# bt_card_type :string(255)
# bt_cardholder_name :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
class Billing < ActiveRecord::Base
belongs_to :user
has_many :transactions, as: :transactable, dependent: :destroy
def name
"#{self.user.name} - Billing"
end
end
# Gem: braintree (2.16.0)
class Brainforest
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :transaction, :success, :result, :billing
attr_accessor :fname, :lname, :email, :address, :address_2, :country, :city, :state_province, :postal_code, :number, :number_encrypted, :expiration_month, :expiration_year, :cvv, :cvv_encrypted, :save_my_billing
def initialize(attributes={})
attributes.each { |key, val| send("#{key}=", val) if respond_to?("#{key}=") }
@attributes = attributes
end
def persisted?
false
end
def valid?
self.errors.empty?
end
def charge(item, price, amount_to_charge, store_in_vault=false, billing=nil)
if amount_to_charge == 0
@transaction = Transaction.new(transactable: item)
@success = true
log_zero_charge(price)
else
validate_billing_fields
if self.valid?
@transaction = Transaction.new(transactable: item)
begin
@result = Braintree::Transaction.sale(
amount: amount_to_charge,
order_id: @transaction.id,
merchant_account_id: Braintree::Configuration.verification_merchant_account_id,
customer: {
first_name: self.fname,
last_name: self.lname,
email: self.email
},
credit_card: {
cardholder_name: "#{self.fname} #{self.lname}",
number: self.number_encrypted || self.number,
expiration_month: self.expiration_month,
expiration_year: self.expiration_year,
cvv: self.cvv_encrypted || self.cvv
},
billing: {
street_address: self.address,
extended_address: self.address_2,
locality: self.city,
region: self.state_province,
postal_code: self.postal_code,
country_code_alpha2: self.country
},
options: {
store_in_vault_on_success: store_in_vault,
submit_for_settlement: true,
add_billing_address_to_payment_method: true
}
)
@success = @result.success?
rescue => e
Airbrake.notify(e)
@result = :error
@success = false
end
log_charge(price, amount_to_charge)
if @success
store_result_in_vault(billing) if store_in_vault
else
set_errors
end
else
@success = false
end
end
end
def validate_billing_fields
[:fname, :lname, :address, :country, :city, :state_province, :postal_code, :number, :expiration_month, :expiration_year, :cvv].each do |attribute|
self.errors.add(attribute, 'is required') if self.send(attribute).blank?
end
end
def log_zero_charge(price)
@transaction.attributes = {
ttype: :charge,
charged: 0,
price: price,
status: 'credited'
}
@transaction.save!
end
def log_charge(price, amount_to_charge)
if @result == :error
@transaction.attributes = {
ttype: :charge,
status: 'braintree_error'
}
elsif @result.transaction
@transaction.attributes = {
ttype: :charge,
approved: @result.success?,
charged: amount_to_charge,
price: price,
bt_transaction_id: @result.transaction.id,
avs_error_response_code: @result.transaction.avs_error_response_code,
avs_postal_code_response_code: @result.transaction.avs_postal_code_response_code,
avs_street_address_response_code: @result.transaction.avs_street_address_response_code,
cvv_response_code: @result.transaction.cvv_response_code,
gateway_rejection_reason: @result.transaction.gateway_rejection_reason,
processor_response_code: @result.transaction.processor_response_code,
processor_response_text: @result.transaction.processor_response_text,
status: @result.transaction.status
}
else
@transaction.attributes = {
ttype: :charge,
approved: @result.success?,
charged: amount_to_charge,
price: price,
validation_errors: @result.errors.collect { |error| "(#{error.code}) #{error.message}" }.join("\n"),
status: 'validation_error'
}
end
@transaction.save!
end
def store_result_in_vault(billing)
billing.update_attributes(
bt_cust_id: @result.transaction.customer_details.id,
bt_cc_token: @result.transaction.credit_card_details.token,
bt_cc_expiration: @result.transaction.credit_card_details.expiration_date,
bt_cc_last4: @result.transaction.credit_card_details.last_4,
bt_card_type: @result.transaction.credit_card_details.card_type,
bt_cardholder_name: @result.transaction.credit_card_details.cardholder_name
)
end
def set_errors
if @result == :error
self.errors.add(:base, 'Credit card could not be processed right now. Please try again later (or right now).')
elsif @result.transaction || @result.credit_card_verification
self.errors.add(:base, 'Credit card could not be processed. Please check that your billing information is correct and try again.')
else
@result.errors.each do |error|
self.errors.add(:base, error.message)
end
end
self.errors[:base].uniq!
end
def charge_from_vault(item, price, amount_to_charge, billing)
if amount_to_charge == 0
@transaction = Transaction.new(transactable: item)
@success = true
log_zero_charge(price)
else
@transaction = Transaction.new(transactable: item)
begin
@result = Braintree::CreditCard.sale(
billing.bt_cc_token,
merchant_account_id: Braintree::Configuration.verification_merchant_account_id,
amount: amount_to_charge,
options: {
submit_for_settlement: true
}
)
@success = @result.success?
rescue
@result = :error
@success = false
end
log_charge(price, amount_to_charge)
set_errors unless @success
end
end
def update_billing(billing)
validate_billing_fields
if self.valid?
@transaction = Transaction.new(transactable: billing)
begin
@result = Braintree::Customer.update(billing.bt_cust_id,
first_name: self.fname,
last_name: self.lname,
email: self.email,
credit_card: {
options: {
update_existing_token: billing.bt_cc_token,
verify_card: true,
verification_merchant_account_id: Braintree::Configuration.verification_merchant_account_id
},
cardholder_name: "#{self.fname} #{self.lname}",
number: self.number_encrypted || self.number,
expiration_month: self.expiration_month,
expiration_year: self.expiration_year,
cvv: self.cvv_encrypted || self.cvv,
billing_address: {
street_address: address,
locality: self.city,
region: self.state_province,
postal_code: self.postal_code,
country_code_alpha2: self.country,
options: {
update_existing: true
}
}
}
)
@success = @result.success?
rescue => e
Airbrake.notify(e)
@result = :error
@success = false
end
log_billing_verification(billing)
if @success
billing.update_attributes(
bt_cust_id: result.customer.id.to_s,
bt_cc_token: result.customer.credit_cards[0].token,
bt_cc_expiration: result.customer.credit_cards[0].expiration_date,
bt_cc_last4: result.customer.credit_cards[0].last_4,
bt_card_type: result.customer.credit_cards[0].card_type,
bt_cardholder_name: result.customer.credit_cards[0].cardholder_name
)
else
set_errors
end
else
@success = false
end
end
def log_billing_verification(billing)
if @result == :error
@transaction.attributes = {
transactable: billing,
ttype: :billing_verification,
}
elsif @result.success?
@transaction.attributes = {
transactable: billing,
ttype: :billing_verification,
status: 'verified'
}
elsif @result.credit_card_verification
@transaction.attributes = {
transactable: billing,
ttype: :billing_verification,
avs_error_response_code: @result.credit_card_verification.avs_error_response_code,
avs_postal_code_response_code: @result.credit_card_verification.avs_postal_code_response_code,
avs_street_address_response_code: @result.credit_card_verification.avs_street_address_response_code,
cvv_response_code: @result.credit_card_verification.cvv_response_code,
gateway_rejection_reason: @result.credit_card_verification.gateway_rejection_reason,
processor_response_code: @result.credit_card_verification.processor_response_code,
processor_response_text: @result.credit_card_verification.processor_response_text,
status: @result.credit_card_verification.status,
validation_errors: @result.errors.collect { |error| "(#{error.code}) #{error.message}" }.join("\n")
}
else
@transaction.attributes = {
transactable: billing,
ttype: :billing_verification,
validation_errors: @result.errors.collect { |error| "(#{error.code}) #{error.message}" }.join("\n"),
status: 'validation_error'
}
end
@transaction.save!
end
end
FactoryGirl.define do
factory :brainforest do
trait :verifiable do
fname { Faker::Name.first_name }
lname { Faker::Name.last_name }
email { Faker::Internet.email }
address { Faker::Address.street_address }
address_2 { Faker::Address.secondary_address }
country { 'US' }
city { Faker::Address.city }
state_province { Faker::AddressUS.state_abbr }
postal_code { Faker::AddressUS.zip_code }
expiration_month '12'
expiration_year '15'
number '4111111111111111'
cvv '123'
end
end
end
require 'spec_helper'
describe Brainforest do
let(:user) { FactoryGirl.create(:user) }
let(:enrollment) { FactoryGirl.create(:enrollment, user: user) }
let(:other_enrollment) { FactoryGirl.create(:enrollment, user: user) }
let(:invalid_billing_error) { 'Credit card could not be processed. Please check that your billing information is correct and try again.' }
context 'validation errors' do
it 'should validate required fields' do
brainforest = FactoryGirl.build(:brainforest, :verifiable, city: '')
brainforest.charge(enrollment, 75, 75)
brainforest.success.should be_false
brainforest.errors[:city].should include('is required')
end
end
context 'verification errors' do
it 'should require a valid number', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '1234123412341234')
brainforest.charge(enrollment, 75, 75)
brainforest.success.should be_false
brainforest.errors[:base].should include('Credit card number is invalid.')
brainforest.errors[:base].should include('Credit card type is not accepted by this merchant account.')
brainforest.transaction.transactable.should eq(enrollment)
brainforest.transaction.ttype.should eq(:charge)
brainforest.transaction.status.should eq('validation_error')
end
it 'should require a valid cvv', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable, cvv: '200')
brainforest.charge(enrollment, 75, 75)
brainforest.success.should be_false
brainforest.errors[:base].should include(invalid_billing_error)
brainforest.transaction.status.should eq('gateway_rejected')
brainforest.transaction.cvv_response_code.should eq('N')
end
it 'should require a valid postal postal code', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable, postal_code: '20000')
brainforest.charge(enrollment, 75, 75)
brainforest.success.should be_false
brainforest.errors[:base].should include(invalid_billing_error)
brainforest.transaction.status.should eq('gateway_rejected')
brainforest.transaction.avs_postal_code_response_code.should eq('N')
end
end
context 'gateway/processor declines' do
it 'should decline a charge', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 2001, 2001)
brainforest.success.should be_false
brainforest.errors[:base].should include(invalid_billing_error)
brainforest.transaction.status.should eq('processor_declined')
brainforest.transaction.processor_response_code.should eq('2001')
end
it 'should decline a charge from the processor', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 2070, 2070)
brainforest.success.should be_false
brainforest.errors[:base].should include(invalid_billing_error)
brainforest.transaction.status.should eq('processor_declined')
brainforest.transaction.processor_response_code.should eq('2070')
end
it 'should decline when the network is unavailable', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 3000, 3000)
brainforest.success.should be_false
brainforest.errors[:base].should include(invalid_billing_error)
brainforest.transaction.status.should eq('failed')
brainforest.transaction.processor_response_code.should eq('3000')
end
end
context 'zero dollar charge' do
it 'should successfully charge zero dollars' do
brainforest = FactoryGirl.build(:brainforest)
brainforest.charge(enrollment, 75, 0)
brainforest.success.should be_true
brainforest.errors[:base].should be_empty
brainforest.transaction.status.should eq('credited')
brainforest.transaction.price.should eq(75)
brainforest.transaction.charged.should eq(0)
end
it 'should successfully charge zero dollars using saved billing' do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
other_brainforest = FactoryGirl.build(:brainforest, :verifiable)
other_brainforest.charge_from_vault(other_enrollment, 75, 0, user.billing)
other_brainforest.success.should be_true
other_brainforest.transaction.status.should eq('credited')
other_brainforest.transaction.price.should eq(75)
other_brainforest.transaction.charged.should eq(0)
end
end
context 'successful charge' do
it 'should charge for an item', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75)
brainforest.success.should be_true
brainforest.errors[:base].should be_empty
brainforest.transaction.status.should eq('submitted_for_settlement')
brainforest.transaction.price.should eq(75)
brainforest.transaction.charged.should eq(75)
end
it 'should charge for a discounted item', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 65)
brainforest.success.should be_true
brainforest.errors[:base].should be_empty
brainforest.transaction.status.should eq('submitted_for_settlement')
brainforest.transaction.price.should eq(75)
brainforest.transaction.charged.should eq(65)
end
it 'should charge for an item and store in the vault', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
brainforest.success.should be_true
brainforest.errors[:base].should be_empty
brainforest.transaction.status.should eq('submitted_for_settlement')
brainforest.transaction.price.should eq(75)
brainforest.transaction.charged.should eq(75)
user.billing.bt_card_type.should eq('Visa')
user.billing.bt_cc_last4.should eq('1111')
end
end
context 'updating vault via transaction' do
it 'should not update a user billing when declined', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '4012000077777777')
brainforest.charge(other_enrollment, 2001, 2001, true, user.billing)
user.billing.bt_cc_last4.should eq('1111')
end
it 'should update a user billing when approved', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
other_brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '4012000077777777')
other_brainforest.charge(other_enrollment, 75, 75, true, user.billing)
user.billing.bt_cc_last4.should eq('7777')
end
end
context 'charging from vault' do
it 'should decline a transaction from the vault', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
other_brainforest = FactoryGirl.build(:brainforest, :verifiable)
other_brainforest.charge_from_vault(other_enrollment, 2001, 2001, user.billing)
other_brainforest.success.should be_false
other_brainforest.transaction.transactable.should eq(other_enrollment)
other_brainforest.transaction.ttype.should eq(:charge)
other_brainforest.transaction.status.should eq('processor_declined')
end
it 'should approve a transaction from the vault', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
other_brainforest = FactoryGirl.build(:brainforest, :verifiable)
other_brainforest.charge_from_vault(other_enrollment, 75, 75, user.billing)
other_brainforest.success.should be_true
other_brainforest.transaction.transactable.should eq(other_enrollment)
other_brainforest.transaction.ttype.should eq(:charge)
other_brainforest.transaction.status.should eq('submitted_for_settlement')
end
end
context 'update billing' do
it 'should not save a card with an invalid number', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '1234123412341234')
brainforest.update_billing(user.billing)
brainforest.success.should be_false
brainforest.errors[:base].should include('Credit card number is invalid.')
brainforest.transaction.transactable.should eq(user.billing)
brainforest.transaction.status.should eq('validation_error')
end
it 'should not save a card with an unverifiable number', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '5105105105105100')
brainforest.update_billing(user.billing)
brainforest.success.should be_false
brainforest.errors[:base].should include(invalid_billing_error)
brainforest.transaction.transactable.should eq(user.billing)
brainforest.transaction.status.should eq('processor_declined')
end
it 'should not save a card with an unmatched CVV', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
brainforest = FactoryGirl.build(:brainforest, :verifiable, cvv: '200')
brainforest.update_billing(user.billing)
brainforest.success.should be_false
brainforest.errors[:base].should include(invalid_billing_error)
brainforest.transaction.transactable.should eq(user.billing)
brainforest.transaction.status.should eq('gateway_rejected')
brainforest.transaction.cvv_response_code.should eq('N')
end
it 'should not save a card with an invalid zip code', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
brainforest = FactoryGirl.build(:brainforest, :verifiable, postal_code: '20000')
brainforest.update_billing(user.billing)
brainforest.success.should be_false
brainforest.errors[:base].should include(invalid_billing_error)
brainforest.transaction.transactable.should eq(user.billing)
brainforest.transaction.status.should eq('gateway_rejected')
brainforest.transaction.avs_postal_code_response_code.should eq('N')
end
it 'should update billing', :vcr do
brainforest = FactoryGirl.build(:brainforest, :verifiable)
brainforest.charge(enrollment, 75, 75, true, user.build_billing)
brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '4012000077777777')
brainforest.update_billing(user.billing)
brainforest.success.should be_true
brainforest.transaction.transactable.should eq(user.billing)
brainforest.transaction.status.should eq('verified')
user.billing.bt_cc_last4.should eq('7777')
end
end
end
# == Schema Information
#
# Table name: transactions
#
# id :integer not null, primary key
# transactable_id :integer
# transactable_type :string(255)
# ttype_cd :integer
# approved :boolean
# charged :decimal(8, 2)
# status :string(255)
# bt_transaction_id :string(255)
# avs_error_response_code :string(255)
# avs_postal_code_response_code :string(255)
# avs_street_address_response_code :string(255)
# cvv_response_code :string(255)
# gateway_rejection_reason :string(255)
# processor_authorization_code :string(255)
# processor_response_code :string(255)
# processor_response_text :string(255)
# validation_errors :text
# created_at :datetime not null
# updated_at :datetime not null
# price :decimal(8, 2)
#
class Transaction < ActiveRecord::Base
belongs_to :transactable, polymorphic: true
as_enum :ttype, billing_verification: 1, charge: 2
def name
self.created_at.strftime("%b %d, %Y %I:%M %p")
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment