Skip to content

Commit

Permalink
save ct api errors as errored payment records. if request to ct api .…
Browse files Browse the repository at this point in the history
…create fails, post payment form to payment error endpoint, which saves a new payment to the campaign with status='error' and accompanying information.

add jquery function to serialize form object instead of field-by-field assignment

move serializeObject() coffeescript definition to main.js.coffee

require `params` parameter for basic_payment_info instead of relying on external definition of params

delete sensitive attributes specifically from form data before posting errored payment

keep track of ct_request_ids for tokenization and charge/auth requests. keep track of errors and timestamps for both.
  • Loading branch information
swyman committed Jan 31, 2014
1 parent 798b717 commit df41eea
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 58 deletions.
32 changes: 25 additions & 7 deletions app/assets/javascripts/campaigns.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,38 @@ Crowdhoster.campaigns =


cardResponseHandler: (response) ->
form = document.getElementById('payment_form')
request_id_token = response.request_id

# store our new request_id
previous_token_elem = $("input[name='ct_tokenize_request_id']")
if (previous_token_elem.length > 0)
previous_token_elem.attr('value', request_id_token)
else
request_input = $('<input name="ct_tokenize_request_id" value="' + request_id_token + '" type="hidden" />');
form.appendChild(request_input[0])

$('#client_timestamp').val((new Date()).getTime())
switch response.status
when 201
token = response.card.id
input = $('<input name="ct_card_id" value="' + token + '" type="hidden" />');
form = document.getElementById('payment_form')
form.appendChild(input[0])
$('#client_timestamp').val((new Date()).getTime())
card_token = response.card.id
card_input = $('<input name="ct_card_id" value="' + card_token + '" type="hidden" />');
form.appendChild(card_input[0])
form.submit()
else
# show an error, re-enable the form submit button, save a record of errored payment
$('#refresh-msg').hide()
$('#errors').append('<p>An error occurred. Please check your credit card details and try again.</p><br><p>If you continue to experience issues, please <a href="mailto:[email protected]?subject=Support request for a payment issue&body=PLEASE DESCRIBE YOUR PAYMENT ISSUES HERE">click here</a> to contact support.</p>')
$('#errors').show()
$('.loader').hide()
$button = $('button[type="submit"]')
$button.attr('disabled', false).html('Confirm payment of $' + $button.attr('data-total') )
$('#card_number').attr('name', 'card_number');
$('#security_code').attr('name', 'security_code');

data = $(form).serializeObject()
data.ct_tokenize_request_error_id = response.error_id
# make sure we don't have sensitive info
delete data.card_number
delete data.security_code

error_path = form.getAttribute('data-error-action')
$.post(error_path, data)
18 changes: 18 additions & 0 deletions app/assets/javascripts/main.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ window.Crowdhoster =

$('.show_tooltip').tooltip()

$.fn.serializeObject = ->
arrayData = @serializeArray()
objectData = {}
$.each arrayData, ->
if @value?
value = @value
else
value = ''

if objectData[@name]?
unless objectData[@name].push
objectData[@name] = [objectData[@name]]

objectData[@name].push value
else
objectData[@name] = value
objectData

$ ->
Crowdhoster.init()
Crowdhoster.admin.init()
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def check_init
def calculate_processing_fee(amount_cents)
amount_cents *= Rails.configuration.processing_fee_percentage.to_f / 100
amount_cents += Rails.configuration.processing_fee_flat_cents
return amount_cents.ceil
amount_cents.ceil
end

end
98 changes: 54 additions & 44 deletions app/controllers/campaigns_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def checkout_payment
if params.has_key?(:reward) && params[:reward].to_i != 0
begin
@reward = Reward.find(params[:reward])
rescue => exception
rescue StandardError => exception
redirect_to checkout_amount_url(@campaign), flash: { info: "This reward is unavailable. Please select a different reward!" }
return
end
Expand All @@ -65,29 +65,12 @@ def checkout_payment
end

def checkout_process

client_timestamp = params.has_key?(:client_timestamp) ? params[:client_timestamp].to_i : nil
ct_user_id = params[:ct_user_id]
ct_card_id = params[:ct_card_id]
fullname = params[:fullname]
email = params[:email]
billing_postal_code = params[:billing_postal_code]

#calculate amount and fee in cents
amount = (params[:amount].to_f*100).ceil
fee = calculate_processing_fee(amount)
quantity = params[:quantity].to_i

#Shipping Info
address_one = params.has_key?(:address_one) ? params[:address_one] : ''
address_two = params.has_key?(:address_two) ? params[:address_two] : ''
city = params.has_key?(:city) ? params[:city] : ''
state = params.has_key?(:state) ? params[:state] : ''
postal_code = params.has_key?(:postal_code) ? params[:postal_code] : ''
country = params.has_key?(:country) ? params[:country] : ''

#Additional Info
additional_info = params.has_key?(:additional_info) ? params[:additional_info] : ''

@reward = false
if params[:reward].to_i != 0
Expand All @@ -114,20 +97,8 @@ def checkout_process
# TODO: Check to make sure the amount is valid here

# Create the payment record in our db, if there are errors, redirect the user
payment_params = {client_timestamp: client_timestamp,
fullname: fullname,
email: email,
billing_postal_code: billing_postal_code,
quantity: quantity,
address_one: address_one,
address_two: address_two,
city: city,
state: state,
postal_code: postal_code,
country: country,
additional_info: additional_info}

@payment = @campaign.payments.new(payment_params)
payment_params = basic_payment_info(params)
@payment = @campaign.payments.new(payment_params)

if !@payment.valid?
error_messages = @payment.errors.full_messages.join(', ')
Expand All @@ -136,7 +107,7 @@ def checkout_process

# Check if there's an existing payment with the same payment_params and client_timestamp.
# If exists, look at the status to route accordingly.
if !client_timestamp.nil? && existing_payment = @campaign.payments.where(payment_params).first
if !payment_params[:client_timestamp].nil? && (existing_payment = @campaign.payments.where(payment_params).first)
case existing_payment.status
when nil
flash_msg = { info: "Your payment is still being processed! If you have not received a confirmation email, please try again or contact support by emailing [email protected]" }
Expand All @@ -160,32 +131,39 @@ def checkout_process
user_id: ct_user_id,
card_id: ct_card_id,
metadata: {
fullname: fullname,
email: email,
billing_postal_code: billing_postal_code,
quantity: quantity,
fullname: payment_params[:fullname],
email: payment_params[:email],
billing_postal_code: payment_params[:billing_postal_code],
quantity: payment_params[:quantity],
reward: @reward ? @reward.id : 0,
additional_info: additional_info
additional_info: payment_params[:additional_info]
}
}
@campaign.production_flag ? Crowdtilt.production(@settings) : Crowdtilt.sandbox

logger.info "CROWDTILT API REQUEST: /campaigns/#{@campaign.ct_campaign_id}/payments"
logger.info payment

response = Crowdtilt.post('/campaigns/' + @campaign.ct_campaign_id + '/payments', {payment: payment})

logger.info "CROWDTILT API RESPONSE:"
logger.info response
rescue => exception
@payment.update_attribute(:status, 'error')
logger.info "ERROR WITH POST TO /payments: #{exception.message}"
rescue Crowdtilt::ApiError => api_error
response = api_error.response
logger.error "API ERROR WITH POST TO /payments: #{response.status} #{response.body}"
error_attributes = {status: 'error'}
error_attributes[:ct_charge_request_id] = response.body['request_id'] if response.body['request_id']
error_attributes[:ct_charge_request_error_id] = response.body['error_id'] if response.body['error_id']
@payment.update_attributes(error_attributes)
redirect_to checkout_amount_url(@campaign), flash: { error: "There was an error processing your payment. Please try again or contact support by emailing [email protected]" } and return
rescue StandardError => exception
@payment.update_attributes({status: 'error'})
logger.error "ERROR WITH POST TO /payments: #{exception.message}"
redirect_to checkout_amount_url(@campaign), flash: { error: "There was an error processing your payment. Please try again or contact support by emailing [email protected]" } and return
end

# Sync payment data
@payment.reward = @reward if @reward
@payment.update_api_data(response['payment'])
@payment.ct_charge_request_id = response['request_id']
@payment.save

# Sync campaign data
Expand All @@ -212,7 +190,17 @@ def checkout_confirmation
end
end

private
def checkout_error
payment_info = basic_payment_info(params)
payment_info[:ct_tokenize_request_error_id] = params[:ct_tokenize_request_error_id]
payment_info[:status] = 'error'
payment = @campaign.payments.new(payment_info)
payment.save

render nothing: true
end

private

def load_campaign
@campaign = Campaign.find(params[:id])
Expand All @@ -234,4 +222,26 @@ def check_exp
end
end

# create simple payment hash from params. does not include fees/payment amounts/cc info.
# to be used in response to javascript payment-creation requests (eg checkout_process and checkout_error)
def basic_payment_info(params)
{
client_timestamp: params.has_key?(:client_timestamp) ? params[:client_timestamp].to_i : nil,
ct_tokenize_request_id: params[:ct_tokenize_request_id],
fullname: params[:fullname],
email: params[:email],
billing_postal_code: params[:billing_postal_code],
quantity: params[:quantity].to_i,

#Shipping Info
address_one: params.has_key?(:address_one) ? params[:address_one] : '',
address_two: params.has_key?(:address_two) ? params[:address_two] : '',
city: params.has_key?(:city) ? params[:city] : '',
state: params.has_key?(:state) ? params[:state] : '',
postal_code: params.has_key?(:postal_code) ? params[:postal_code] : '',
country: params.has_key?(:country) ? params[:country] : '',
additional_info: params.has_key?(:additional_info) ? params[:additional_info] : ''
}
end

end
4 changes: 3 additions & 1 deletion app/models/payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ class Payment < ActiveRecord::Base
attr_accessible :ct_payment_id, :status, :amount, :user_fee_amount, :admin_fee_amount, :fullname, :email,
:card_type, :card_last_four, :card_expiration_month, :card_expiration_year, :billing_postal_code,
:address_one, :address_two, :city, :state, :postal_code, :country, :quantity,
:additional_info, :client_timestamp
:additional_info, :client_timestamp,
:ct_charge_request_id, :ct_charge_request_error_id,
:ct_tokenize_request_id, :ct_tokenize_request_error_id

validates :fullname, :quantity, presence: true
validates :email, presence: true, email: true
Expand Down
2 changes: 1 addition & 1 deletion app/views/campaigns/checkout_payment.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<div class="well checkout_block" style="padding-bottom: 16px">

<form accept-charset="UTF-8" action="<%= checkout_process_path(@campaign) %>" method="post" id="payment_form">
<form accept-charset="UTF-8" action="<%= checkout_process_path(@campaign) %>" method="post" id="payment_form" data-error-action="<%= checkout_error_path(@campaign) %>">

<input name="authenticity_token" type="hidden" value="<%= form_authenticity_token %>">
<h4 class="contact">Contact Information</h4>
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
match '/:id/checkout/payment', to: 'campaigns#checkout_payment', as: :checkout_payment
match '/:id/checkout/process', to: 'campaigns#checkout_process', as: :checkout_process
match '/:id/checkout/confirmation', to: 'campaigns#checkout_confirmation', as: :checkout_confirmation
post '/:id/checkout/error', to: 'campaigns#checkout_error', as: :checkout_error
match '/:id', to: 'campaigns#home', as: :campaign_home


Expand Down
8 changes: 8 additions & 0 deletions db/migrate/20140128001609_add_errors_to_payments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AddErrorsToPayments < ActiveRecord::Migration
def change
add_column :payments, :ct_tokenize_request_id, :string
add_column :payments, :ct_tokenize_request_error_id, :string
add_column :payments, :ct_charge_request_id, :string
add_column :payments, :ct_charge_request_error_id, :string
end
end
12 changes: 8 additions & 4 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.

ActiveRecord::Schema.define(:version => 20140116122844) do
ActiveRecord::Schema.define(:version => 20140128001609) do

create_table "campaigns", :force => true do |t|
t.string "name"
Expand Down Expand Up @@ -113,8 +113,8 @@
t.string "card_expiration_month"
t.string "card_expiration_year"
t.integer "campaign_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.string "address_one"
t.string "address_two"
t.string "city"
Expand All @@ -125,7 +125,11 @@
t.integer "reward_id"
t.text "additional_info"
t.string "billing_postal_code"
t.integer "client_timestamp", :limit => 8
t.integer "client_timestamp", :limit => 8
t.string "ct_tokenize_request_id"
t.string "ct_tokenize_request_error_id"
t.string "ct_charge_request_id"
t.string "ct_charge_request_error_id"
end

create_table "rewards", :force => true do |t|
Expand Down

0 comments on commit df41eea

Please sign in to comment.