Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration of the Shopify Active Merchant Logic #1

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
source 'https://rubygems.org'

group :test, :remote_test do
gem 'pry-rails'
end

gemspec

gem 'jruby-openssl', :platforms => :jruby
Expand Down
3 changes: 2 additions & 1 deletion activemerchant.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
134 changes: 134 additions & 0 deletions lib/active_merchant/billing/gateways/shopify.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
require 'shopify_api'

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'

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
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 = {})
refunder = ShopifyRefunder.new(money, transaction_id, options)
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)
@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?

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, :order_id
end

class ShopifyRefunder
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: order_id })
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?
perform_refund_on_shopify
elsif partial_refund?
perform_refund_on_shopify
else
raise ActiveMerchant::Billing::ShopifyGateway::CreditedAmountBiggerThanTransaction
end
end

private

def perform_refund_on_shopify
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?
credited_money == amount_to_cents(transaction.amount)
end

def partial_refund?
credited_money < amount_to_cents(transaction.amount)
end

def amount_to_cents(amount)
BigDecimal.new(amount) * 100
end

def amount_to_dollars(amount)
BigDecimal.new(amount) / 100
end

attr_accessor :credited_money, :refund_reason, :transaction, :order_id
end
6 changes: 6 additions & 0 deletions test/fixtures.yml
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,12 @@ spreedly_core:
password: "Y2i7AjgU03SUjwY4xnOPqzdsv4dMbPDCQzorAk8Bcoy0U8EIVE4innGjuoMQv7MN"
gateway_token: "3gLeg4726V5P0HK7cq7QzHsL0a6"

# Replace with your own credentials
shopify:
api_key: "use"
password: "your"
shop_name: "own"

# Working credentials, no need to replace
stripe:
login: sk_test_3OD4TdKSIOhDOL2146JJcC79
Expand Down
2 changes: 2 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
87 changes: 87 additions & 0 deletions test/unit/gateways/remote_shopify_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
require 'test_helper'

class RemoteStripeTest < Test::Unit::TestCase
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

@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, @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

private

def create_fulfilled_paid_shopify_order
order = ::ShopifyAPI::Order.new
order.email = '[email protected]'
order.test = true
order.fulfillment_status = 'fulfilled'
order.line_items = [
{
variant_id: '447654529',
quantity: 1,
name: 'test',
price: @refund_amount,
title: 'title'
}
]
order.customer = { first_name: 'Paul',
last_name: 'Norman',
email: '[email protected]' }

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: 'capture',
status: 'success',
amount: @refund_amount
}
]
order.financial_status = 'paid'
order.save

order
end
end
55 changes: 55 additions & 0 deletions test/unit/gateways/shopify_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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_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') }
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: 1)
::ShopifyAPI::Transaction.stubs(:find).returns(transaction)
assert_raises(::ActiveMerchant::Billing::ShopifyGateway::CreditedAmountBiggerThanTransaction) { @gateway.refund(10000, 123, { order_id: '123', reason: 'reason' }) }
end

def test_response_value_of_unsuccessful_refund
transaction_id = 123
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