From 3b78179deabb9192a02cf2e9f8811a92055ebecf Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Mon, 31 Jul 2017 12:46:41 +1000 Subject: [PATCH] Ask customer about saving card details used in checkout --- .../darkswarm/services/checkout.js.coffee | 1 + app/models/spree/credit_card_decorator.rb | 8 + app/models/spree/payment_decorator.rb | 1 + .../spree/checkout/payment/_stripe.html.haml | 5 + config/locales/en.yml | 1 + spec/requests/stripe_connect_checkout_spec.rb | 183 +++++++++++------- 6 files changed, 128 insertions(+), 71 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/checkout.js.coffee b/app/assets/javascripts/darkswarm/services/checkout.js.coffee index 7d19c71799..cc17e4e032 100644 --- a/app/assets/javascripts/darkswarm/services/checkout.js.coffee +++ b/app/assets/javascripts/darkswarm/services/checkout.js.coffee @@ -74,6 +74,7 @@ Darkswarm.factory 'Checkout', ($injector, CurrentOrder, ShippingMethods, StripeJ year: @secrets.card.exp_year first_name: @order.bill_address.firstname last_name: @order.bill_address.lastname + save_requested_by_customer: @secrets.save_requested_by_customer } munged_order diff --git a/app/models/spree/credit_card_decorator.rb b/app/models/spree/credit_card_decorator.rb index 01b8bc0eba..69dadd6707 100644 --- a/app/models/spree/credit_card_decorator.rb +++ b/app/models/spree/credit_card_decorator.rb @@ -4,6 +4,10 @@ Spree::CreditCard.class_eval do # Obviously can be removed once we are using strong params attr_accessible :cc_type, :last_digits + # For holding customer preference in memory + attr_accessible :save_requested_by_customer + attr_writer :save_requested_by_customer + # Should be able to remove once we reach Spree v2.2.0 # https://github.com/spree/spree/commit/411010f3975c919ab298cb63962ee492455b415c belongs_to :payment_method @@ -15,4 +19,8 @@ Spree::CreditCard.class_eval do def has_payment_profile? # rubocop:disable Style/PredicateName gateway_customer_profile_id.present? || gateway_payment_profile_id.present? end + + def save_requested_by_customer? + !!@save_requested_by_customer + end end diff --git a/app/models/spree/payment_decorator.rb b/app/models/spree/payment_decorator.rb index 568d82371a..84e3aa6d51 100644 --- a/app/models/spree/payment_decorator.rb +++ b/app/models/spree/payment_decorator.rb @@ -94,6 +94,7 @@ module Spree def create_payment_profile return unless source.is_a?(CreditCard) + return unless source.try(:save_requested_by_customer?) return unless source.number || source.gateway_payment_profile_id return unless source.gateway_customer_profile_id.nil? payment_method.create_profile(self) diff --git a/app/views/spree/checkout/payment/_stripe.html.haml b/app/views/spree/checkout/payment/_stripe.html.haml index 885156dd5c..398636cdd3 100644 --- a/app/views/spree/checkout/payment/_stripe.html.haml +++ b/app/views/spree/checkout/payment/_stripe.html.haml @@ -10,5 +10,10 @@ %div{ ng: { if: '!secrets.selected_card' } } = render "spree/checkout/payment/gateway", payment_method: payment_method +.row + .small-12.columns.text-right + = check_box_tag 'secrets.save_requested_by_customer' + = label_tag 'secrets.save_requested_by_customer', t('.remember_this_card') + :javascript Stripe.setPublishableKey("#{ENV['STRIPE_INSTANCE_PUBLISHABLE_KEY']}") diff --git a/config/locales/en.yml b/config/locales/en.yml index 56b7163f4b..e5d630e4cb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2161,6 +2161,7 @@ Please follow the instructions there to make your enterprise visible on the Open enter_new_card: Enter details for a new card used_saved_card: "Use a saved card:" or_enter_new_card: "Or, enter details for a new card:" + remember_this_card: Remember this card? date_picker: format: ! '%Y-%m-%d' js_format: 'yy-mm-dd' diff --git a/spec/requests/stripe_connect_checkout_spec.rb b/spec/requests/stripe_connect_checkout_spec.rb index f89b681e89..d5cc332126 100644 --- a/spec/requests/stripe_connect_checkout_spec.rb +++ b/spec/requests/stripe_connect_checkout_spec.rb @@ -35,83 +35,124 @@ describe "Submitting Stripe Connect charge requests", type: :request do context "when a new card is submitted" do let(:store_response_mock) { { status: 200, body: JSON.generate(id: customer_id, default_card: card_id, sources: { data: [{id: "1"}] }) } } - let(:charge_response_mock) { { status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) } } let(:token_response_mock) { { status: 200, body: JSON.generate(id: new_token) } } + let(:charge_response_mock) { { status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) } } - before do - # Saves the card against the user - stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/customers") - .with(:body => { card: token, email: order.email}) - .to_return(store_response_mock) - - # Requests a token from the newly saved card - stub_request(:post, "https://api.stripe.com/v1/tokens") - .with(:body => { card: card_id, customer: customer_id}) - .to_return(token_response_mock) - - # Charges the card - stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/charges") - .with(:body => {"amount" => "1234", "card" => new_token, "currency" => "aud", "description" => "Spree Order ID: #{order.number}", "payment_user_agent" => "Stripe/v1 ActiveMerchantBindings/1.63.0"}) - .to_return(charge_response_mock) - end - - context "and the store, token and charge requests are successful" do - it "should process the payment, and stores the card/customer details" do - put update_checkout_path, params - json_response = JSON.parse(response.body) - expect(json_response["path"]).to eq spree.order_path(order) - expect(order.payments.completed.count).to be 1 - card = order.payments.completed.first.source - expect(card.gateway_customer_profile_id).to eq customer_id - expect(card.gateway_payment_profile_id).to eq card_id - expect(card.cc_type).to eq "visa" - expect(card.last_digits).to eq "4242" - expect(card.first_name).to eq "Jill" - expect(card.last_name).to eq "Jeffreys" - end - end - - context "when the store request returns an error message" do - let(:store_response_mock) { { status: 402, body: JSON.generate(error: { message: "Bup-bow..."}) } } - - it "should not process the payment" do - put update_checkout_path, params - expect(response.status).to be 400 - json_response = JSON.parse(response.body) - expect(json_response["flash"]["error"]).to eq I18n.t(:spree_gateway_error_flash_for_checkout, error: 'Bup-bow...') - expect(order.payments.completed.count).to be 0 - end - end - - context "when the charge request returns an error message" do - let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "Bup-bow..."}) } } - - it "should not process the payment" do - put update_checkout_path, params - expect(response.status).to be 400 - json_response = JSON.parse(response.body) - expect(json_response["flash"]["error"]).to eq I18n.t(:payment_processing_failed) - expect(order.payments.completed.count).to be 0 - end - end - - context "when the token request returns an error message" do - let(:token_response_mock) { { status: 402, body: JSON.generate(error: { message: "Bup-bow..."}) } } - let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "Bup-bow..."}) } } - + context "and the user doesn't request that the card is saved for later" do before do - # Attempts to charge the card without a token, which will return an error + # Charges the card stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/charges") - .with(:body => {"amount" => "1234", "currency" => "aud", "description" => "Spree Order ID: #{order.number}", "payment_user_agent" => "Stripe/v1 ActiveMerchantBindings/1.63.0"}) + .with(:body => {"amount" => "1234", "card" => token, "currency" => "aud", "description" => "Spree Order ID: #{order.number}", "payment_user_agent" => "Stripe/v1 ActiveMerchantBindings/1.63.0"}) .to_return(charge_response_mock) end - it "should not process the payment" do - put update_checkout_path, params - expect(response.status).to be 400 - json_response = JSON.parse(response.body) - expect(json_response["flash"]["error"]).to eq I18n.t(:payment_processing_failed) - expect(order.payments.completed.count).to be 0 + context "and the charge request is successful" do + it "should process the payment without storing card details" do + put update_checkout_path, params + json_response = JSON.parse(response.body) + expect(json_response["path"]).to eq spree.order_path(order) + expect(order.payments.completed.count).to be 1 + card = order.payments.completed.first.source + expect(card.gateway_customer_profile_id).to eq nil + expect(card.gateway_payment_profile_id).to eq token + expect(card.cc_type).to eq "visa" + expect(card.last_digits).to eq "4242" + expect(card.first_name).to eq "Jill" + expect(card.last_name).to eq "Jeffreys" + end + end + + context "when the charge request returns an error message" do + let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "Bup-bow..."}) } } + + it "should not process the payment" do + put update_checkout_path, params + expect(response.status).to be 400 + json_response = JSON.parse(response.body) + expect(json_response["flash"]["error"]).to eq I18n.t(:payment_processing_failed) + expect(order.payments.completed.count).to be 0 + end + end + end + + context "and the customer requests that the card is saved for later" do + before do + params[:order][:payments_attributes][0][:source_attributes][:save_requested_by_customer] = '1' + + # Saves the card against the user + stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/customers") + .with(:body => { card: token, email: order.email}) + .to_return(store_response_mock) + + # Requests a token from the newly saved card + stub_request(:post, "https://api.stripe.com/v1/tokens") + .with(:body => { card: card_id, customer: customer_id}) + .to_return(token_response_mock) + + # Charges the card + stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/charges") + .with(:body => {"amount" => "1234", "card" => new_token, "currency" => "aud", "description" => "Spree Order ID: #{order.number}", "payment_user_agent" => "Stripe/v1 ActiveMerchantBindings/1.63.0"}) + .to_return(charge_response_mock) + end + + context "and the store, token and charge requests are successful" do + it "should process the payment, and stores the card/customer details" do + put update_checkout_path, params + json_response = JSON.parse(response.body) + expect(json_response["path"]).to eq spree.order_path(order) + expect(order.payments.completed.count).to be 1 + card = order.payments.completed.first.source + expect(card.gateway_customer_profile_id).to eq customer_id + expect(card.gateway_payment_profile_id).to eq card_id + expect(card.cc_type).to eq "visa" + expect(card.last_digits).to eq "4242" + expect(card.first_name).to eq "Jill" + expect(card.last_name).to eq "Jeffreys" + end + end + + context "when the store request returns an error message" do + let(:store_response_mock) { { status: 402, body: JSON.generate(error: { message: "Bup-bow..."}) } } + + it "should not process the payment" do + put update_checkout_path, params + expect(response.status).to be 400 + json_response = JSON.parse(response.body) + expect(json_response["flash"]["error"]).to eq I18n.t(:spree_gateway_error_flash_for_checkout, error: 'Bup-bow...') + expect(order.payments.completed.count).to be 0 + end + end + + context "when the charge request returns an error message" do + let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "Bup-bow..."}) } } + + it "should not process the payment" do + put update_checkout_path, params + expect(response.status).to be 400 + json_response = JSON.parse(response.body) + expect(json_response["flash"]["error"]).to eq I18n.t(:payment_processing_failed) + expect(order.payments.completed.count).to be 0 + end + end + + context "when the token request returns an error message" do + let(:token_response_mock) { { status: 402, body: JSON.generate(error: { message: "Bup-bow..."}) } } + let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "Bup-bow..."}) } } + + before do + # Attempts to charge the card without a token, which will return an error + stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/charges") + .with(:body => {"amount" => "1234", "currency" => "aud", "description" => "Spree Order ID: #{order.number}", "payment_user_agent" => "Stripe/v1 ActiveMerchantBindings/1.63.0"}) + .to_return(charge_response_mock) + end + + it "should not process the payment" do + put update_checkout_path, params + expect(response.status).to be 400 + json_response = JSON.parse(response.body) + expect(json_response["flash"]["error"]).to eq I18n.t(:payment_processing_failed) + expect(order.payments.completed.count).to be 0 + end end end end @@ -131,8 +172,8 @@ describe "Submitting Stripe Connect charge requests", type: :request do ) end - let(:charge_response_mock) { { status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) } } let(:token_response_mock) { { status: 200, body: JSON.generate(id: new_token) } } + let(:charge_response_mock) { { status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) } } before do params[:order][:existing_card] = credit_card.id