From af80977706e3711a336f2477c82812f519f9afb2 Mon Sep 17 00:00:00 2001 From: Charles-Andre Bouffard Date: Tue, 9 Aug 2016 15:29:40 -0400 Subject: [PATCH 1/9] Add the Shopify Gateway logic in its own file Previously that code was residing in the Spree Gateway itself, but in the end it made more sense to have it there. --- activemerchant.gemspec | 1 + .../billing/gateways/shopify.rb | 113 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 lib/active_merchant/billing/gateways/shopify.rb diff --git a/activemerchant.gemspec b/activemerchant.gemspec index 5c322135..87755081 100644 --- a/activemerchant.gemspec +++ b/activemerchant.gemspec @@ -30,4 +30,5 @@ Gem::Specification.new do |s| s.add_development_dependency('test-unit', '~> 3') s.add_development_dependency('mocha', '~> 1') s.add_development_dependency('thor') + s.add_development_dependency('shopify_api', '~> 4.0') end diff --git a/lib/active_merchant/billing/gateways/shopify.rb b/lib/active_merchant/billing/gateways/shopify.rb new file mode 100644 index 00000000..0b386e88 --- /dev/null +++ b/lib/active_merchant/billing/gateways/shopify.rb @@ -0,0 +1,113 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class ShopifyGateway < Gateway + class TransactionNotFoundError < Error; end + + self.homepage_url = 'https://shopify.ca/' + self.display_name = 'Shopify' + + def initialize(options = {}) + requires!(options, :login) + @api_key = options[:api_key] + @password = options[:password] + @shop_name = options[:shop_name] + init_shopify_api! + + super + end + + def void(transaction_id, options = {}) + order_id = options[:order_id] + voider = ShopifyVoider.new(transaction_id, order_id) + voider.perform + end + + def refund(money, transaction_id, options = {}) + refund = options[:originator] + refunder = ShopifyRefunder.new(money, transaction_id, refund) + refunder.perform + end + + private + + attr_reader :api_key, :password, :shop_name + + def init_shopify_api! + ::ShopifyAPI::Base.site = shop_url + end + + def shop_url + "https://#{api_key}:#{password}@#{shop_name}" + end + end + end +end + +class ShopifyVoider + def initialize(transaction_id, order_id) + @transaction = ::ShopifyAPI::Transaction.find(transaction_id, params: { order_id: order_id }) + end + + def perform + raise TransactionNotFoundError if transaction.nil? + + transaction.kind = 'void' + transaction.save + end + + private + + attr_reader :transaction +end + +class ShopifyRefunder + def initialize(credited_money, transaction_id, refund) + @refund = refund + @credited_money = BigDecimal.new(credited_money) + @transaction = ::ShopifyAPI::Transaction.find(transaction_id, params: { order_id: pos_order_id }) + end + + def perform + if full_refund? + perform_full_refund_on_shopify + elsif partial_refund? + raise NotImplementedError + else + raise NotImplementedError + end + end + + private + + def perform_full_refund_on_shopify + ::ShopifyAPI::Refund.create({ shipping: { full_refund: true }, + note: refund.reason.name, + notify: false, + restock: false, + transaction: suggested_transaction }, + params: { order_id: pos_order_id }) + end + + def suggested_transaction + ::ShopifyAPI::Refund.calculate({ shipping: { full_refund: true } }, + params: { order_id: pos_order_id }) + end + + def pos_order_id + refund.pos_order_id + end + + def full_refund? + credited_money == amount_to_cents(transaction.amount) + end + + def partial_refund? + BigDecimal.new(credit_money) < amount_to_cents(transaction.amount) + end + + def amount_to_cents(amount) + amount * 100 + end + + attr_accessor :credited_money, :refund, :transaction +end From 1279c1133e9992953dcf623cd6fa995473c0520f Mon Sep 17 00:00:00 2001 From: Charles-Andre Bouffard Date: Tue, 9 Aug 2016 16:07:22 -0400 Subject: [PATCH 2/9] Requires the right parameters for the Shopify gateway We required "login" when it doesn't even exists in our scenario --- lib/active_merchant/billing/gateways/shopify.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/active_merchant/billing/gateways/shopify.rb b/lib/active_merchant/billing/gateways/shopify.rb index 0b386e88..1a1dc12b 100644 --- a/lib/active_merchant/billing/gateways/shopify.rb +++ b/lib/active_merchant/billing/gateways/shopify.rb @@ -7,7 +7,9 @@ class TransactionNotFoundError < Error; end self.display_name = 'Shopify' def initialize(options = {}) - requires!(options, :login) + requires!(options, :api_key) + requires!(options, :password) + requires!(options, :shop_name) @api_key = options[:api_key] @password = options[:password] @shop_name = options[:shop_name] From d146a6424d8a1ecb9c60e140546dc30de5830d6d Mon Sep 17 00:00:00 2001 From: Charles-Andre Bouffard Date: Tue, 9 Aug 2016 16:13:54 -0400 Subject: [PATCH 3/9] Increment the minimum ruby version to allow shopify_api gem Bleeding edge, they say... --- activemerchant.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activemerchant.gemspec b/activemerchant.gemspec index 87755081..e17fe676 100644 --- a/activemerchant.gemspec +++ b/activemerchant.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |s| s.homepage = 'http://activemerchant.org/' s.rubyforge_project = 'activemerchant' - s.required_ruby_version = '>= 2' + s.required_ruby_version = '>= 2.3' s.files = Dir['CHANGELOG', 'README.md', 'MIT-LICENSE', 'CONTRIBUTORS', 'lib/**/*', 'vendor/**/*'] s.require_path = 'lib' From 6c3735ff8b57884e3cd9d373f79ffdab153e6407 Mon Sep 17 00:00:00 2001 From: Charles-Andre Bouffard Date: Tue, 9 Aug 2016 17:22:07 -0400 Subject: [PATCH 4/9] Add really basic test to ensure that the gem is usable We had trouble with the gem not being usable due to not requiring the gem at the top of the Shopify Gateway. --- lib/active_merchant/billing/gateways/shopify.rb | 5 ++++- test/unit/gateways/shopify_test.rb | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 test/unit/gateways/shopify_test.rb diff --git a/lib/active_merchant/billing/gateways/shopify.rb b/lib/active_merchant/billing/gateways/shopify.rb index 1a1dc12b..5be1917a 100644 --- a/lib/active_merchant/billing/gateways/shopify.rb +++ b/lib/active_merchant/billing/gateways/shopify.rb @@ -1,3 +1,5 @@ +require 'shopify_api' + module ActiveMerchant #:nodoc: module Billing #:nodoc: class ShopifyGateway < Gateway @@ -51,7 +53,8 @@ def initialize(transaction_id, order_id) end def perform - raise TransactionNotFoundError if transaction.nil? + raise ActiveMerchant::Billing::ShopifyGateway::TransactionNotFoundError if transaction.nil? + transaction.kind = 'void' transaction.save diff --git a/test/unit/gateways/shopify_test.rb b/test/unit/gateways/shopify_test.rb new file mode 100644 index 00000000..90e02ab3 --- /dev/null +++ b/test/unit/gateways/shopify_test.rb @@ -0,0 +1,14 @@ +require 'test_helper' + +class ShopifyTest < Test::Unit::TestCase + def setup + @gateway = ShopifyGateway.new(api_key: 'api_key', + password: 'password', + shop_name: 'shop_name') + end + + def test_void_with_not_found_transaction + ::ShopifyAPI::Transaction.expects(:find).returns(nil) + assert_raises(::ActiveMerchant::Billing::ShopifyGateway::TransactionNotFoundError) { @gateway.void(123, order_id: '123') } + end +end From ffcbac5ba51fc367985ad30fe24356dcd62e9d4e Mon Sep 17 00:00:00 2001 From: Charles-Andre Bouffard Date: Wed, 10 Aug 2016 11:13:44 -0400 Subject: [PATCH 5/9] WIP - Start writing tests for RemoteShopifyGateway We want to make sure that the order gets correctly refunded or voided when calling the respective methods. --- Gemfile | 1 + .../billing/gateways/shopify.rb | 39 +++++----- test/fixtures.yml | 6 ++ test/unit/gateways/remote_shopify_test.rb | 75 +++++++++++++++++++ 4 files changed, 100 insertions(+), 21 deletions(-) create mode 100644 test/unit/gateways/remote_shopify_test.rb diff --git a/Gemfile b/Gemfile index 7c62a122..12d00ec3 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ gemspec gem 'jruby-openssl', :platforms => :jruby +gem 'pry' group :test, :remote_test do # gateway-specific dependencies, keeping these gems out of the gemspec gem 'braintree', '>= 2.50.0' diff --git a/lib/active_merchant/billing/gateways/shopify.rb b/lib/active_merchant/billing/gateways/shopify.rb index 5be1917a..3a4c115d 100644 --- a/lib/active_merchant/billing/gateways/shopify.rb +++ b/lib/active_merchant/billing/gateways/shopify.rb @@ -12,9 +12,11 @@ def initialize(options = {}) requires!(options, :api_key) requires!(options, :password) requires!(options, :shop_name) + @api_key = options[:api_key] @password = options[:password] @shop_name = options[:shop_name] + init_shopify_api! super @@ -27,8 +29,7 @@ def void(transaction_id, options = {}) end def refund(money, transaction_id, options = {}) - refund = options[:originator] - refunder = ShopifyRefunder.new(money, transaction_id, refund) + refunder = ShopifyRefunder.new(money, transaction_id, options) refunder.perform end @@ -55,7 +56,6 @@ def initialize(transaction_id, order_id) def perform raise ActiveMerchant::Billing::ShopifyGateway::TransactionNotFoundError if transaction.nil? - transaction.kind = 'void' transaction.save end @@ -66,10 +66,11 @@ def perform end class ShopifyRefunder - def initialize(credited_money, transaction_id, refund) - @refund = refund + def initialize(credited_money, transaction_id, options) + @refund_reason = options[:reason] + @order_id = options[:order_id] @credited_money = BigDecimal.new(credited_money) - @transaction = ::ShopifyAPI::Transaction.find(transaction_id, params: { order_id: pos_order_id }) + @transaction = ::ShopifyAPI::Transaction.find(transaction_id, params: { order_id: order_id }) end def perform @@ -85,21 +86,17 @@ def perform private def perform_full_refund_on_shopify - ::ShopifyAPI::Refund.create({ shipping: { full_refund: true }, - note: refund.reason.name, - notify: false, - restock: false, - transaction: suggested_transaction }, - params: { order_id: pos_order_id }) + ::ShopifyAPI::Refund.create(order_id: order_id, + shipping: { full_refund: true }, + note: refund_reason, + notify: false, + restock: false, + transaction: suggested_transaction) end def suggested_transaction - ::ShopifyAPI::Refund.calculate({ shipping: { full_refund: true } }, - params: { order_id: pos_order_id }) - end - - def pos_order_id - refund.pos_order_id + ::ShopifyAPI::Refund.calculate(shipping: { amount: credited_money }, + params: { order_id: order_id }) end def full_refund? @@ -107,12 +104,12 @@ def full_refund? end def partial_refund? - BigDecimal.new(credit_money) < amount_to_cents(transaction.amount) + credited_money < amount_to_cents(transaction.amount) end def amount_to_cents(amount) - amount * 100 + BigDecimal.new(amount) * 100 end - attr_accessor :credited_money, :refund, :transaction + attr_accessor :credited_money, :refund_reason, :transaction, :order_id end diff --git a/test/fixtures.yml b/test/fixtures.yml index 8586621d..e2d2e8a4 100644 --- a/test/fixtures.yml +++ b/test/fixtures.yml @@ -942,6 +942,12 @@ spreedly_core: password: "Y2i7AjgU03SUjwY4xnOPqzdsv4dMbPDCQzorAk8Bcoy0U8EIVE4innGjuoMQv7MN" gateway_token: "3gLeg4726V5P0HK7cq7QzHsL0a6" +# Replace with your own credentials +shopify: + api_key: "d5719fb9c960c45ad1418675b9c725c4" + password: "4ac8651236acbd5e5ffccc8c5458c4df" + shop_name: "dynamo-staging.myshopify.com/admin" + # Working credentials, no need to replace stripe: login: sk_test_3OD4TdKSIOhDOL2146JJcC79 diff --git a/test/unit/gateways/remote_shopify_test.rb b/test/unit/gateways/remote_shopify_test.rb new file mode 100644 index 00000000..5959a349 --- /dev/null +++ b/test/unit/gateways/remote_shopify_test.rb @@ -0,0 +1,75 @@ +require 'test_helper' + +class RemoteStripeTest < Test::Unit::TestCase + def setup + @gateway = ShopifyGateway.new(fixtures(:shopify)) + + @order = create_shopify_order + @transaction = ::ShopifyAPI::Order.find(@order.id).transactions.first + @amount = BigDecimal.new(@transaction.amount) * 100 + + @options = { order_id: @order.id, reason: 'Object is malfunctioning' } + end + + def teardown + @order.destroy + end + + def test_successful_full_refund + assert response = @gateway.refund(@amount, @transaction.id, @options) + assert_success response + end + + private + + def create_shopify_order + order = ::ShopifyAPI::Order.new + order.email = 'cab@godynamo.com' + order.fulfillment_status = 'partial' + order.line_items = [ + { + variant_id: '447654529', + quantity: 1, + name: 'test', + price: 140, + title: 'title' + } + ] + order.customer = { first_name: 'Paul', + last_name: 'Norman', + email: 'paul.norman@example.com' } + + order.billing_address = { + first_name: 'John', + last_name: 'Smith', + address1: '123 Fake Street', + phone: '555-555-5555', + city: 'Fakecity', + province: 'Ontario', + country: 'Canada', + zip: 'K2P 1L4' + } + order.shipping_address = { + first_name: 'John', + last_name: 'Smith', + address1: '123 Fake Street', + phone: '555-555-5555', + city: 'Fakecity', + province: 'Ontario', + country: 'Canada', + zip: 'K2P 1L4' + } + order.transactions = [ + { + kind: 'authorization', + status: 'success', + amount: 50.0 + } + ] + order.financial_status = 'partially_paid' + order.save + + order + end +end + From 381bda6ea5bc8604acb2184bef1473ef36cdeab9 Mon Sep 17 00:00:00 2001 From: Charles-Andre Bouffard Date: Wed, 10 Aug 2016 12:00:35 -0400 Subject: [PATCH 6/9] Allow the an order to be refunded We are still receiving an error in the API response which makes the test fail, but in Shopify we see that the order has no errors and has been correctly refunded. I want to see if this implementation still works when doing the full cycle. --- Gemfile | 6 ++++- .../billing/gateways/shopify.rb | 24 ++++++++++--------- test/test_helper.rb | 2 ++ test/unit/gateways/remote_shopify_test.rb | 20 ++++++++-------- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/Gemfile b/Gemfile index 12d00ec3..d1466712 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,13 @@ source 'https://rubygems.org' + +group :test, :remote_test do + gem 'pry-rails' +end + gemspec gem 'jruby-openssl', :platforms => :jruby -gem 'pry' group :test, :remote_test do # gateway-specific dependencies, keeping these gems out of the gemspec gem 'braintree', '>= 2.50.0' diff --git a/lib/active_merchant/billing/gateways/shopify.rb b/lib/active_merchant/billing/gateways/shopify.rb index 3a4c115d..1d196ec7 100644 --- a/lib/active_merchant/billing/gateways/shopify.rb +++ b/lib/active_merchant/billing/gateways/shopify.rb @@ -74,10 +74,12 @@ def initialize(credited_money, transaction_id, options) end def perform + # NOTE(cab): This should be refactored when we are sure that this is the + # behavior we want if full_refund? - perform_full_refund_on_shopify + perform_refund_on_shopify elsif partial_refund? - raise NotImplementedError + perform_refund_on_shopify else raise NotImplementedError end @@ -85,18 +87,18 @@ def perform private - def perform_full_refund_on_shopify + def perform_refund_on_shopify ::ShopifyAPI::Refund.create(order_id: order_id, - shipping: { full_refund: true }, + shipping: { amount: 0 }, note: refund_reason, notify: false, restock: false, - transaction: suggested_transaction) - end - - def suggested_transaction - ::ShopifyAPI::Refund.calculate(shipping: { amount: credited_money }, - params: { order_id: order_id }) + transactions: [{ + parent_id: transaction.id, + amount: @credited_money, + gateway: 'shopify-payments', + kind: 'refund' + }]) end def full_refund? @@ -108,7 +110,7 @@ def partial_refund? end def amount_to_cents(amount) - BigDecimal.new(amount) * 100 + BigDecimal.new(amount) end attr_accessor :credited_money, :refund_reason, :transaction, :order_id diff --git a/test/test_helper.rb b/test/test_helper.rb index c80e72c3..0a89ff09 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -11,6 +11,8 @@ require 'active_merchant' require 'comm_stub' +require 'pry' + require 'active_support/core_ext/integer/time' require 'active_support/core_ext/numeric/time' require 'active_support/core_ext/time/acts_like' diff --git a/test/unit/gateways/remote_shopify_test.rb b/test/unit/gateways/remote_shopify_test.rb index 5959a349..1f0db636 100644 --- a/test/unit/gateways/remote_shopify_test.rb +++ b/test/unit/gateways/remote_shopify_test.rb @@ -4,9 +4,9 @@ class RemoteStripeTest < Test::Unit::TestCase def setup @gateway = ShopifyGateway.new(fixtures(:shopify)) - @order = create_shopify_order + @refund_amount = 50 + @order = create_fulfilled_paid_shopify_order @transaction = ::ShopifyAPI::Order.find(@order.id).transactions.first - @amount = BigDecimal.new(@transaction.amount) * 100 @options = { order_id: @order.id, reason: 'Object is malfunctioning' } end @@ -16,22 +16,23 @@ def teardown end def test_successful_full_refund - assert response = @gateway.refund(@amount, @transaction.id, @options) + assert response = @gateway.refund(@refund_amount, @transaction.id, @options) assert_success response end private - def create_shopify_order + def create_fulfilled_paid_shopify_order order = ::ShopifyAPI::Order.new order.email = 'cab@godynamo.com' - order.fulfillment_status = 'partial' + order.test = true + order.fulfillment_status = 'fulfilled' order.line_items = [ { variant_id: '447654529', quantity: 1, name: 'test', - price: 140, + price: @refund_amount, title: 'title' } ] @@ -61,15 +62,14 @@ def create_shopify_order } order.transactions = [ { - kind: 'authorization', + kind: 'capture', status: 'success', - amount: 50.0 + amount: @refund_amount } ] - order.financial_status = 'partially_paid' + order.financial_status = 'paid' order.save order end end - From 9147e1407a3f081590a1776288b04b37561e0498 Mon Sep 17 00:00:00 2001 From: Charles-Andre Bouffard Date: Wed, 10 Aug 2016 13:11:56 -0400 Subject: [PATCH 7/9] Improve spec suite and make use of proper amount usage The amount that is given by solidus is always in cents, we were not taking that in consideration previously. --- .../billing/gateways/shopify.rb | 11 ++++++++-- test/unit/gateways/remote_shopify_test.rb | 3 ++- test/unit/gateways/shopify_test.rb | 20 +++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/active_merchant/billing/gateways/shopify.rb b/lib/active_merchant/billing/gateways/shopify.rb index 1d196ec7..754929d7 100644 --- a/lib/active_merchant/billing/gateways/shopify.rb +++ b/lib/active_merchant/billing/gateways/shopify.rb @@ -4,6 +4,7 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class ShopifyGateway < Gateway class TransactionNotFoundError < Error; end + class CreditedAmountBiggerThanTransaction < Error; end self.homepage_url = 'https://shopify.ca/' self.display_name = 'Shopify' @@ -74,6 +75,8 @@ def initialize(credited_money, transaction_id, options) end def perform + raise ActiveMerchant::Billing::ShopifyGateway::TransactionNotFoundError if transaction.nil? + # NOTE(cab): This should be refactored when we are sure that this is the # behavior we want if full_refund? @@ -81,7 +84,7 @@ def perform elsif partial_refund? perform_refund_on_shopify else - raise NotImplementedError + raise ActiveMerchant::Billing::ShopifyGateway::CreditedAmountBiggerThanTransaction end end @@ -95,7 +98,7 @@ def perform_refund_on_shopify restock: false, transactions: [{ parent_id: transaction.id, - amount: @credited_money, + amount: amount_to_dollars(credited_money), gateway: 'shopify-payments', kind: 'refund' }]) @@ -113,5 +116,9 @@ def amount_to_cents(amount) BigDecimal.new(amount) end + def amount_to_dollars(amount) + BigDecimal.new(amount) / 100 + end + attr_accessor :credited_money, :refund_reason, :transaction, :order_id end diff --git a/test/unit/gateways/remote_shopify_test.rb b/test/unit/gateways/remote_shopify_test.rb index 1f0db636..c47a8b80 100644 --- a/test/unit/gateways/remote_shopify_test.rb +++ b/test/unit/gateways/remote_shopify_test.rb @@ -5,6 +5,7 @@ def setup @gateway = ShopifyGateway.new(fixtures(:shopify)) @refund_amount = 50 + @refund_amount_in_cents = @refund_amount * 100 @order = create_fulfilled_paid_shopify_order @transaction = ::ShopifyAPI::Order.find(@order.id).transactions.first @@ -16,7 +17,7 @@ def teardown end def test_successful_full_refund - assert response = @gateway.refund(@refund_amount, @transaction.id, @options) + assert response = @gateway.refund(@refund_amount_in_cents, @transaction.id, @options) assert_success response end diff --git a/test/unit/gateways/shopify_test.rb b/test/unit/gateways/shopify_test.rb index 90e02ab3..65ed6552 100644 --- a/test/unit/gateways/shopify_test.rb +++ b/test/unit/gateways/shopify_test.rb @@ -11,4 +11,24 @@ def test_void_with_not_found_transaction ::ShopifyAPI::Transaction.expects(:find).returns(nil) assert_raises(::ActiveMerchant::Billing::ShopifyGateway::TransactionNotFoundError) { @gateway.void(123, order_id: '123') } end + + def test_refund_with_not_found_transaction + ::ShopifyAPI::Transaction.expects(:find).returns(nil) + assert_raises(::ActiveMerchant::Billing::ShopifyGateway::TransactionNotFoundError) { @gateway.refund(123, 123, { order_id: '123', reason: 'reason' }) } + end + + def test_refund_with_credit_to_big + transaction = stub(amount: 100) + ::ShopifyAPI::Transaction.stubs(:find).returns(transaction) + assert_raises(::ActiveMerchant::Billing::ShopifyGateway::CreditedAmountBiggerThanTransaction) { @gateway.refund(1000, 123, { order_id: '123', reason: 'reason' }) } + end + + def test_full_refund + transaction_id = 123 + transaction = stub(amount: 100, id: transaction_id) + refund = stub(success?: true) + ::ShopifyAPI::Transaction.stubs(:find).returns(transaction) + ::ShopifyAPI::Refund.stubs(:create).returns(refund) + assert_success(@gateway.refund(100, transaction_id, { order_id: '123', reason: 'reason' })) + end end From e799340081846de3cd164adc3de905c13a97e424 Mon Sep 17 00:00:00 2001 From: Charles-Andre Bouffard Date: Wed, 10 Aug 2016 13:46:22 -0400 Subject: [PATCH 8/9] Return a proper response value when refunding a Shopify order For a yet unknown reason, Shopify can return an error object without an actual error. In that case, we have to check if there is an error message associated with that object, if there is no error message then we assume that it has successfully did it's thing. --- .../billing/gateways/shopify.rb | 31 ++++++++++++------- test/unit/gateways/shopify_test.rb | 20 +++++++++--- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/lib/active_merchant/billing/gateways/shopify.rb b/lib/active_merchant/billing/gateways/shopify.rb index 754929d7..197c40da 100644 --- a/lib/active_merchant/billing/gateways/shopify.rb +++ b/lib/active_merchant/billing/gateways/shopify.rb @@ -91,17 +91,24 @@ def perform private def perform_refund_on_shopify - ::ShopifyAPI::Refund.create(order_id: order_id, - shipping: { amount: 0 }, - note: refund_reason, - notify: false, - restock: false, - transactions: [{ - parent_id: transaction.id, - amount: amount_to_dollars(credited_money), - gateway: 'shopify-payments', - kind: 'refund' - }]) + refund = ::ShopifyAPI::Refund.create(order_id: order_id, + shipping: { amount: 0 }, + note: refund_reason, + notify: false, + restock: false, + transactions: [{ + parent_id: transaction.id, + amount: amount_to_dollars(credited_money), + gateway: 'shopify-payments', + kind: 'refund' + }]) + + success = refund.errors == [] + if success || refund.errors.messages.empty? + ActiveMerchant::Billing::Response.new(true, nil) + else + ActiveMerchant::Billing::Response.new(success, refund.errors.messages) + end end def full_refund? @@ -113,7 +120,7 @@ def partial_refund? end def amount_to_cents(amount) - BigDecimal.new(amount) + BigDecimal.new(amount) * 100 end def amount_to_dollars(amount) diff --git a/test/unit/gateways/shopify_test.rb b/test/unit/gateways/shopify_test.rb index 65ed6552..458a976a 100644 --- a/test/unit/gateways/shopify_test.rb +++ b/test/unit/gateways/shopify_test.rb @@ -18,17 +18,27 @@ def test_refund_with_not_found_transaction end def test_refund_with_credit_to_big - transaction = stub(amount: 100) + transaction = stub(amount: 1) ::ShopifyAPI::Transaction.stubs(:find).returns(transaction) - assert_raises(::ActiveMerchant::Billing::ShopifyGateway::CreditedAmountBiggerThanTransaction) { @gateway.refund(1000, 123, { order_id: '123', reason: 'reason' }) } + assert_raises(::ActiveMerchant::Billing::ShopifyGateway::CreditedAmountBiggerThanTransaction) { @gateway.refund(10000, 123, { order_id: '123', reason: 'reason' }) } end - def test_full_refund + def test_response_value_of_unsuccessful_refund transaction_id = 123 - transaction = stub(amount: 100, id: transaction_id) - refund = stub(success?: true) + transaction = stub(amount: 1, id: transaction_id) + refund = stub(errors: []) ::ShopifyAPI::Transaction.stubs(:find).returns(transaction) ::ShopifyAPI::Refund.stubs(:create).returns(refund) assert_success(@gateway.refund(100, transaction_id, { order_id: '123', reason: 'reason' })) end + + def test_reponse_value_of_successful_refund + transaction_id = 123 + transaction = stub(amount: 1, id: transaction_id) + errors = stub(messages: { error: 'error1' }) + refund = stub(errors: errors) + ::ShopifyAPI::Transaction.stubs(:find).returns(transaction) + ::ShopifyAPI::Refund.stubs(:create).returns(refund) + assert_failure(@gateway.refund(100, transaction_id, { order_id: '123', reason: 'reason' })) + end end From affb7f71bace3419a8d4f1f325e5c00b4f97e566 Mon Sep 17 00:00:00 2001 From: Charles-Andre Bouffard Date: Wed, 10 Aug 2016 14:23:48 -0400 Subject: [PATCH 9/9] Do a full refund when voiding a payment Since Shopify-Payment gateway uses Stripe and Stripe doesn't support voiding, we simply use our Refunder object and refund for the total amount of the order. --- lib/active_merchant/billing/gateways/shopify.rb | 9 ++++++--- test/fixtures.yml | 6 +++--- test/unit/gateways/remote_shopify_test.rb | 15 +++++++++++++-- test/unit/gateways/shopify_test.rb | 11 +++++++++++ 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/active_merchant/billing/gateways/shopify.rb b/lib/active_merchant/billing/gateways/shopify.rb index 197c40da..4117870d 100644 --- a/lib/active_merchant/billing/gateways/shopify.rb +++ b/lib/active_merchant/billing/gateways/shopify.rb @@ -51,19 +51,22 @@ def shop_url class ShopifyVoider def initialize(transaction_id, order_id) + @order_id = order_id @transaction = ::ShopifyAPI::Transaction.find(transaction_id, params: { order_id: order_id }) end def perform raise ActiveMerchant::Billing::ShopifyGateway::TransactionNotFoundError if transaction.nil? - transaction.kind = 'void' - transaction.save + options = { order_id: order_id, reason: 'Payment voided' } + full_amount_to_cents = BigDecimal.new(transaction.amount) * 100 + refunder = ShopifyRefunder.new(full_amount_to_cents, transaction.id, options) + refunder.perform end private - attr_reader :transaction + attr_reader :transaction, :order_id end class ShopifyRefunder diff --git a/test/fixtures.yml b/test/fixtures.yml index e2d2e8a4..51323f92 100644 --- a/test/fixtures.yml +++ b/test/fixtures.yml @@ -944,9 +944,9 @@ spreedly_core: # Replace with your own credentials shopify: - api_key: "d5719fb9c960c45ad1418675b9c725c4" - password: "4ac8651236acbd5e5ffccc8c5458c4df" - shop_name: "dynamo-staging.myshopify.com/admin" + api_key: "use" + password: "your" + shop_name: "own" # Working credentials, no need to replace stripe: diff --git a/test/unit/gateways/remote_shopify_test.rb b/test/unit/gateways/remote_shopify_test.rb index c47a8b80..8db61e7b 100644 --- a/test/unit/gateways/remote_shopify_test.rb +++ b/test/unit/gateways/remote_shopify_test.rb @@ -9,15 +9,26 @@ def setup @order = create_fulfilled_paid_shopify_order @transaction = ::ShopifyAPI::Order.find(@order.id).transactions.first - @options = { order_id: @order.id, reason: 'Object is malfunctioning' } + @refund_options = { order_id: @order.id, reason: 'Object is malfunctioning' } + @void_options = { order_id: @order.id, reason: 'Payment voided' } end def teardown @order.destroy end + def test_successful_void + assert response = @gateway.void(@transaction.id, @void_options) + assert_success response + end + def test_successful_full_refund - assert response = @gateway.refund(@refund_amount_in_cents, @transaction.id, @options) + assert response = @gateway.refund(@refund_amount_in_cents, @transaction.id, @refund_options) + assert_success response + end + + def test_successful_partial_refund + assert response = @gateway.refund(@refund_amount_in_cents / 2, @transaction.id, @refund_options) assert_success response end diff --git a/test/unit/gateways/shopify_test.rb b/test/unit/gateways/shopify_test.rb index 458a976a..f9bb5e34 100644 --- a/test/unit/gateways/shopify_test.rb +++ b/test/unit/gateways/shopify_test.rb @@ -7,6 +7,17 @@ def setup shop_name: 'shop_name') end + def test_void_calls_refund + transaction_id = 123 + transaction = stub(amount: 1, id: transaction_id) + refunder_instance = stub(perform: true) + ::ShopifyAPI::Transaction.expects(:find).returns(transaction) + ShopifyRefunder.expects(:new).returns(refunder_instance) + + refunder_instance.expects(:perform).once + @gateway.void(123, { order_id: '123' }) + end + def test_void_with_not_found_transaction ::ShopifyAPI::Transaction.expects(:find).returns(nil) assert_raises(::ActiveMerchant::Billing::ShopifyGateway::TransactionNotFoundError) { @gateway.void(123, order_id: '123') }