From df41eead571fe3a4cd25ab39c53d6287d8651660 Mon Sep 17 00:00:00 2001 From: Sam Wyman Date: Tue, 28 Jan 2014 11:02:25 -0800 Subject: [PATCH] save ct api errors as errored payment records. if request to ct api .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. --- app/assets/javascripts/campaigns.js.coffee | 32 ++++-- app/assets/javascripts/main.js.coffee | 18 ++++ app/controllers/application_controller.rb | 2 +- app/controllers/campaigns_controller.rb | 98 ++++++++++--------- app/models/payment.rb | 4 +- app/views/campaigns/checkout_payment.html.erb | 2 +- config/routes.rb | 1 + .../20140128001609_add_errors_to_payments.rb | 8 ++ db/schema.rb | 12 ++- 9 files changed, 119 insertions(+), 58 deletions(-) create mode 100644 db/migrate/20140128001609_add_errors_to_payments.rb diff --git a/app/assets/javascripts/campaigns.js.coffee b/app/assets/javascripts/campaigns.js.coffee index 6cd88c54..b4679c39 100644 --- a/app/assets/javascripts/campaigns.js.coffee +++ b/app/assets/javascripts/campaigns.js.coffee @@ -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 = $(''); + form.appendChild(request_input[0]) + + $('#client_timestamp').val((new Date()).getTime()) switch response.status when 201 - token = response.card.id - input = $(''); - form = document.getElementById('payment_form') - form.appendChild(input[0]) - $('#client_timestamp').val((new Date()).getTime()) + card_token = response.card.id + card_input = $(''); + 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('

An error occurred. Please check your credit card details and try again.


If you continue to experience issues, please click here to contact support.

') $('#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) diff --git a/app/assets/javascripts/main.js.coffee b/app/assets/javascripts/main.js.coffee index baf1de89..96c90946 100644 --- a/app/assets/javascripts/main.js.coffee +++ b/app/assets/javascripts/main.js.coffee @@ -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() diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index aa37d257..6acc0cbf 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/campaigns_controller.rb b/app/controllers/campaigns_controller.rb index 9c933b01..5b9a2f51 100644 --- a/app/controllers/campaigns_controller.rb +++ b/app/controllers/campaigns_controller.rb @@ -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 @@ -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 @@ -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(', ') @@ -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 team@crowdhoster.com" } @@ -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 team@crowdhoster.com" } 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 team@crowdhoster.com" } 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 @@ -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]) @@ -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 diff --git a/app/models/payment.rb b/app/models/payment.rb index eb44d3ed..449b1798 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -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 diff --git a/app/views/campaigns/checkout_payment.html.erb b/app/views/campaigns/checkout_payment.html.erb index a8b02611..26ec3236 100644 --- a/app/views/campaigns/checkout_payment.html.erb +++ b/app/views/campaigns/checkout_payment.html.erb @@ -5,7 +5,7 @@
-
+

Contact Information

diff --git a/config/routes.rb b/config/routes.rb index 46bf2e8a..ca0a51ed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20140128001609_add_errors_to_payments.rb b/db/migrate/20140128001609_add_errors_to_payments.rb new file mode 100644 index 00000000..5229bf12 --- /dev/null +++ b/db/migrate/20140128001609_add_errors_to_payments.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 464da1fc..b71192cb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" @@ -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" @@ -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|