active_fulfillment 2.1.6 → 2.1.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e4362a1173e03f71bdcd1f26ca7a643f463ade18
4
- data.tar.gz: 46c3b9a7179584e85496a07a204ceb5ace6d388b
3
+ metadata.gz: 36a68a21c6b752b4ed78e4d11fb5b4da3e05080a
4
+ data.tar.gz: 755d255c35294881ac9c1a815836f0adce457876
5
5
  SHA512:
6
- metadata.gz: 5c071f80b06412e9cec9f4ff381b4853c758320ff3476331b3247437698c80f27157654931c5695e963765622f0b341db1279a043018c26d445ff2546bc9f0be
7
- data.tar.gz: 1d85c23e9a29f9e86e9d5bd316a4126b3887f2735daaf3ebcfd3c3dab56bfabc9e0c3b916539d3959cdaf8f23b92332e323b78c4e835cd10726fc1a2458b51ba
6
+ metadata.gz: 4023404d90d16da1415227009f16348767890d6ea2f8799738e29cc801731f3cee43c50fd6c29b743672f4b6c7a99473383cd6c022129e4bf1112bd4838ec2f7
7
+ data.tar.gz: c15a97a53539e0d597aaa08c1760bcee3fbe751920e9e53ee92b121c824e9bd895a8463059c0e1dabb602624e148d5bdcbaf0db46291a5658612ac8b8612fee2
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG CHANGED
@@ -1,5 +1,10 @@
1
1
  = ActiveFulfillment CHANGELOG
2
2
 
3
+ == Version 2.1.7
4
+
5
+ * Add shopify_api service
6
+ * Drop Ruby 1.9.3 support
7
+
3
8
  == Version 2.1.0
4
9
 
5
10
  * Added fetch_tracking_data methods which returns tracking_companies and tracking_urls if available in addition to tracking_numbers for each service
@@ -5,6 +5,8 @@ module ActiveMerchant
5
5
  include RequiresParameters
6
6
  include PostsData
7
7
 
8
+ attr_accessor :logger
9
+
8
10
  def initialize(options = {})
9
11
  check_test_mode(options)
10
12
 
@@ -20,12 +22,12 @@ module ActiveMerchant
20
22
  @options[:test] || Base.mode == :test
21
23
  end
22
24
 
23
- # API Requirements for Implementors
24
- def fulfill(order_id, shipping_address, line_items, options = {})
25
- raise NotImplementedError.new("Subclasses must implement")
25
+ def valid_credentials?
26
+ true
26
27
  end
27
28
 
28
- def fetch_stock_levels(options = {})
29
+ # API Requirements for Implementors
30
+ def fulfill(order_id, shipping_address, line_items, options = {})
29
31
  raise NotImplementedError.new("Subclasses must implement")
30
32
  end
31
33
 
@@ -44,15 +46,8 @@ module ActiveMerchant
44
46
  raise NotImplementedError.new("Subclasses must implement")
45
47
  end
46
48
 
47
- def valid_credentials?
48
- raise NotImplementedError.new("Subclasses must implement")
49
- end
50
-
51
- def test_mode?
52
- raise NotImplementedError.new("Subclasses must implement")
53
- end
54
-
55
49
  private
50
+
56
51
  def check_test_mode(options)
57
52
  if options[:test] and not test_mode?
58
53
  raise ArgumentError, 'Test mode is not supported by this gateway'
@@ -1,3 +1,4 @@
1
+ require 'active_fulfillment/fulfillment/services/shopify_api'
1
2
  require 'active_fulfillment/fulfillment/services/shipwire'
2
3
  require 'active_fulfillment/fulfillment/services/webgistix'
3
4
  require 'active_fulfillment/fulfillment/services/amazon'
@@ -0,0 +1,123 @@
1
+ module ActiveMerchant
2
+ module Fulfillment
3
+ class ShopifyAPIService < Service
4
+
5
+ RESCUABLE_CONNECTION_ERRORS = [
6
+ Net::ReadTimeout,
7
+ Net::OpenTimeout,
8
+ TimeoutError,
9
+ Errno::ETIMEDOUT,
10
+ Timeout::Error,
11
+ IOError,
12
+ EOFError,
13
+ SocketError,
14
+ Errno::ECONNRESET,
15
+ Errno::ECONNABORTED,
16
+ Errno::EPIPE,
17
+ Errno::ECONNREFUSED,
18
+ Errno::EAGAIN,
19
+ Errno::EHOSTUNREACH,
20
+ Errno::ENETUNREACH,
21
+ Resolv::ResolvError,
22
+ Net::HTTPBadResponse,
23
+ Net::HTTPHeaderSyntaxError,
24
+ Net::ProtocolError,
25
+ ActiveMerchant::ConnectionError,
26
+ ActiveMerchant::ResponseError,
27
+ ActiveMerchant::InvalidResponseError
28
+ ]
29
+
30
+ def initialize(options = {})
31
+ @name = options[:name]
32
+ @callback_url = options[:callback_url]
33
+ @format = options[:format]
34
+ end
35
+
36
+ def fulfill(order_id, shipping_address, line_items, options = {})
37
+ raise NotImplementedError.new("Shopify API Service must listen to fulfillment/create Webhooks")
38
+ end
39
+
40
+ def fetch_stock_levels(options = {})
41
+ response = send_app_request('fetch_stock', options.delete(:headers), options)
42
+ if response
43
+ stock_levels = parse_response(response, 'StockLevels', 'Product', 'Sku', 'Quantity') { |p| p.to_i }
44
+ Response.new(true, "API stock levels", {:stock_levels => stock_levels})
45
+ else
46
+ Response.new(false, "Unable to fetch remote stock levels")
47
+ end
48
+ end
49
+
50
+ def fetch_tracking_data(order_ids, options = {})
51
+ options.merge!({:order_ids => order_ids})
52
+ response = send_app_request('fetch_tracking_numbers', options.delete(:headers), options)
53
+ if response
54
+ tracking_numbers = parse_response(response, 'TrackingNumbers', 'Order', 'ID', 'Tracking') { |o| o }
55
+ Response.new(true, "API tracking_numbers", {:tracking_numbers => tracking_numbers,
56
+ :tracking_companies => {},
57
+ :tracking_urls => {}})
58
+ else
59
+ Response.new(false, "Unable to fetch remote tracking numbers #{order_ids.inspect}")
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def request_uri(action, data)
66
+ URI.parse "#{@callback_url}/#{action}.#{@format}?#{data.to_param}"
67
+ end
68
+
69
+ def send_app_request(action, headers, data)
70
+ uri = request_uri(action, data)
71
+
72
+ logger.info "[" + @name.upcase + " APP] Post #{uri}"
73
+
74
+ response = nil
75
+ realtime = Benchmark.realtime do
76
+ begin
77
+ Timeout.timeout(20.seconds) do
78
+ response = ssl_get(uri, headers)
79
+ end
80
+ rescue *(RESCUABLE_CONNECTION_ERRORS) => e
81
+ logger.warn "[#{self}] Error while contacting fulfillment service error =\"#{e.message}\""
82
+ end
83
+ end
84
+
85
+ line = "[" + @name.upcase + "APP] Response from #{uri} --> "
86
+ line << "#{response} #{"%.4fs" % realtime}"
87
+ logger.info line
88
+
89
+ response
90
+ end
91
+
92
+ def parse_response(response, root, type, key, value)
93
+ case @format
94
+ when 'json'
95
+ response_data = ActiveSupport::JSON.decode(response)
96
+ return {} unless response_data.is_a?(Hash)
97
+ response_data[root.underscore] || response_data
98
+ when 'xml'
99
+ response_data = {}
100
+ document = REXML::Document.new(response)
101
+ document.elements[root].each do |node|
102
+ if node.name == type
103
+ response_data[node.elements[key].text] = node.elements[value].text
104
+ end
105
+ end
106
+ response_data
107
+ end
108
+
109
+ rescue ActiveSupport::JSON.parse_error, REXML::ParseException
110
+ {}
111
+ end
112
+
113
+ def encode_payload(payload, root)
114
+ case @format
115
+ when 'json'
116
+ {root => payload}.to_json
117
+ when 'xml'
118
+ payload.to_xml(:root => root)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
  module ActiveMerchant
3
3
  module Fulfillment
4
- VERSION = "2.1.6"
4
+ VERSION = "2.1.7"
5
5
  end
6
6
  end
data/test/test_helper.rb CHANGED
@@ -9,6 +9,7 @@ require 'minitest/autorun'
9
9
  require 'digest/md5'
10
10
  require 'active_fulfillment'
11
11
  require 'active_utils'
12
+ require 'timecop'
12
13
 
13
14
  require 'mocha/setup'
14
15
 
@@ -16,35 +17,35 @@ module Test
16
17
  module Unit
17
18
  class TestCase < MiniTest::Unit::TestCase
18
19
  include ActiveMerchant::Fulfillment
19
-
20
+
20
21
  LOCAL_CREDENTIALS = ENV['HOME'] + '/.active_merchant/fixtures.yml' unless defined?(LOCAL_CREDENTIALS)
21
22
  DEFAULT_CREDENTIALS = File.dirname(__FILE__) + '/fixtures.yml' unless defined?(DEFAULT_CREDENTIALS)
22
23
 
23
24
  def all_fixtures
24
25
  @@fixtures ||= load_fixtures
25
26
  end
26
-
27
+
27
28
  def fixtures(key)
28
29
  data = all_fixtures[key] || raise(StandardError, "No fixture data was found for '#{key}'")
29
-
30
+
30
31
  data.dup
31
32
  end
32
-
33
+
33
34
  def load_fixtures
34
35
  file = File.exists?(LOCAL_CREDENTIALS) ? LOCAL_CREDENTIALS : DEFAULT_CREDENTIALS
35
36
  yaml_data = YAML.load(File.read(file))
36
37
  symbolize_keys(yaml_data)
37
-
38
+
38
39
  yaml_data
39
40
  end
40
41
 
41
42
  def xml_fixture(path) # where path is like 'usps/beverly_hills_to_ottawa_response'
42
43
  open(File.join(File.dirname(__FILE__),'fixtures','xml',"#{path}.xml")) {|f| f.read}
43
44
  end
44
-
45
+
45
46
  def symbolize_keys(hash)
46
47
  return unless hash.is_a?(Hash)
47
-
48
+
48
49
  hash.symbolize_keys!
49
50
  hash.each{|k,v| symbolize_keys(v)}
50
51
  end
@@ -0,0 +1,134 @@
1
+ require 'test_helper'
2
+
3
+ class ShopifyAPITest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @service = build_service()
7
+ @service.logger = stub(:info => nil, :debug => nil, :warn => nil, :error => nil)
8
+ end
9
+
10
+ def test_request_uri_is_correct_when_no_sku_passed
11
+ Timecop.freeze do
12
+ timestamp = Time.now.utc.to_i
13
+ uri = @service.send(:request_uri, 'fetch_stock', {timestamp: timestamp, shop: 'www.snowdevil.ca'})
14
+ assert_equal "http://supershopifyapptwin.com/fetch_stock.json?shop=www.snowdevil.ca&timestamp=#{timestamp}", uri.to_s
15
+ end
16
+ end
17
+
18
+ def test_request_uri_is_correct_when_sku_is_passed
19
+ Timecop.freeze do
20
+ timestamp = Time.now.utc.to_i
21
+ uri = @service.send(:request_uri, 'fetch_stock', {sku: '123', timestamp: timestamp, shop: 'www.snowdevil.ca'})
22
+ assert_equal "http://supershopifyapptwin.com/fetch_stock.json?shop=www.snowdevil.ca&sku=123&timestamp=#{timestamp}", uri.to_s
23
+ end
24
+ end
25
+
26
+ def test_response_from_failed_stock_request
27
+ mock_app_request('fetch_stock', anything, nil)
28
+ response = @service.fetch_stock_levels()
29
+ refute response.success?
30
+ assert_equal "Unable to fetch remote stock levels", response.message
31
+ end
32
+
33
+ def test_response_from_failed_tracking_request
34
+ mock_app_request('fetch_tracking_numbers', anything, nil)
35
+ response = @service.fetch_tracking_numbers([1,2])
36
+ refute response.success?
37
+ assert_equal "Unable to fetch remote tracking numbers [1, 2]", response.message
38
+ end
39
+
40
+ def test_response_with_invalid_json_is_parsed_to_empty_hash
41
+ bad_json = '{a: 9, 0}'
42
+ mock_app_request('fetch_stock', anything, bad_json)
43
+ assert_equal({}, @service.fetch_stock_levels().stock_levels)
44
+ end
45
+
46
+ def test_response_with_valid_but_incorrect_json_is_parsed_to_empty_hash
47
+ incorrect_json = '[]'
48
+ mock_app_request('fetch_stock', anything, incorrect_json)
49
+ assert_equal({}, @service.fetch_stock_levels().stock_levels)
50
+ end
51
+
52
+ def test_response_with_invalid_xml_is_parsed_to_empty_hash
53
+ service = build_service(format: 'xml')
54
+ bad_xml = '<A><B></C></A>'
55
+ mock_app_request('fetch_stock', anything, bad_xml)
56
+ assert_equal({}, service.fetch_stock_levels().stock_levels)
57
+ end
58
+
59
+ def test_parse_stock_level_response_parses_xml_correctly
60
+ service = build_service(format: 'xml')
61
+ xml = '<StockLevels><Product><Sku>sku1</Sku><Quantity>1</Quantity></Product><Product><Sku>sku2</Sku><Quantity>2</Quantity></Product></StockLevels>'
62
+ expected = {'sku1' => '1', 'sku2' => '2'}
63
+
64
+ mock_app_request('fetch_stock', anything, xml)
65
+ assert_equal expected, service.fetch_stock_levels().stock_levels
66
+ end
67
+
68
+ def test_parse_tracking_data_response_parses_xml_correctly
69
+ service = build_service(format: 'xml')
70
+ xml = '<TrackingNumbers><Order><ID>123</ID><Tracking>abc</Tracking></Order><Order><ID>456</ID><Tracking>def</Tracking></Order></TrackingNumbers>'
71
+ expected = {'123' => 'abc', '456' => 'def'}
72
+
73
+ mock_app_request('fetch_tracking_numbers', anything, xml)
74
+ assert_equal expected, service.fetch_tracking_data([1,2,4]).tracking_numbers
75
+ end
76
+
77
+ def test_parse_stock_level_response_parses_json_with_root_correctly
78
+ json = '{"stock_levels": {"998KIB":"10"}}'
79
+ expected = {'998KIB' => "10"}
80
+
81
+ mock_app_request('fetch_stock', anything, json)
82
+ assert_equal expected, @service.fetch_stock_levels().stock_levels
83
+ end
84
+
85
+ def test_parse_tracking_data_response_parses_json_with_root_correctly
86
+ json = '{"tracking_numbers": {"order1":"a","order2":"b"}}'
87
+ expected = {'order1' => 'a', 'order2' => 'b'}
88
+
89
+ mock_app_request('fetch_tracking_numbers', anything, json)
90
+ assert_equal expected, @service.fetch_tracking_data([1,2]).tracking_numbers
91
+ end
92
+
93
+ def test_parse_stock_level_response_parses_json_without_root_correctly
94
+ json = '{"998KIB":"10"}'
95
+ expected = {'998KIB' => "10"}
96
+
97
+ mock_app_request('fetch_stock', anything, json)
98
+ assert_equal expected, @service.fetch_stock_levels().stock_levels
99
+ end
100
+
101
+ def test_parse_tracking_data_response_parses_json_without_root_correctly
102
+ json = '{"order1":"a","order2":"b"}'
103
+ expected = {'order1' => 'a', 'order2' => 'b'}
104
+
105
+ mock_app_request('fetch_tracking_numbers', anything, json)
106
+ assert_equal expected, @service.fetch_tracking_data([1,2]).tracking_numbers
107
+ end
108
+
109
+ def test_send_app_request_rescues_response_errors
110
+ response = stub(code: "404", message: "Not Found")
111
+ @service.expects(:ssl_get).raises(ActiveMerchant::ResponseError, response)
112
+ refute @service.fetch_stock_levels().success?
113
+ end
114
+
115
+ def test_send_app_request_rescues_invalid_response_errors
116
+ @service.expects(:ssl_get).raises(ActiveMerchant::InvalidResponseError.new("error html"))
117
+ refute @service.fetch_stock_levels().success?
118
+ end
119
+
120
+ private
121
+
122
+ def mock_app_request(action, input, output)
123
+ ShopifyAPIService.any_instance.expects(:send_app_request).with(action, nil, input).returns(output)
124
+ end
125
+
126
+ def build_service(options = {})
127
+ options.reverse_merge!({
128
+ name: "fulfillment_app",
129
+ callback_url: 'http://supershopifyapptwin.com',
130
+ format: 'json'
131
+ })
132
+ ShopifyAPIService.new(options)
133
+ end
134
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_fulfillment
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.6
4
+ version: 2.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Fauser
@@ -31,7 +31,7 @@ cert_chain:
31
31
  fl3hbtVFTqbOlwL9vy1fudXcolIE/ZTcxQ+er07ZFZdKCXayR9PPs64heamfn0fp
32
32
  TConQSX2BnZdhIEYW+cKzEC/bLc=
33
33
  -----END CERTIFICATE-----
34
- date: 2014-07-30 00:00:00.000000000 Z
34
+ date: 2014-09-03 00:00:00.000000000 Z
35
35
  dependencies:
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: activesupport
@@ -89,6 +89,20 @@ dependencies:
89
89
  - - ">="
90
90
  - !ruby/object:Gem::Version
91
91
  version: '0'
92
+ - !ruby/object:Gem::Dependency
93
+ name: byebug
94
+ requirement: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
92
106
  - !ruby/object:Gem::Dependency
93
107
  name: mocha
94
108
  requirement: !ruby/object:Gem::Requirement
@@ -117,6 +131,20 @@ dependencies:
117
131
  - - ">="
118
132
  - !ruby/object:Gem::Version
119
133
  version: '0'
134
+ - !ruby/object:Gem::Dependency
135
+ name: timecop
136
+ requirement: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ type: :development
142
+ prerelease: false
143
+ version_requirements: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
120
148
  - !ruby/object:Gem::Dependency
121
149
  name: rdoc
122
150
  requirement: !ruby/object:Gem::Requirement
@@ -146,6 +174,7 @@ files:
146
174
  - lib/active_fulfillment/fulfillment/services/amazon.rb
147
175
  - lib/active_fulfillment/fulfillment/services/amazon_mws.rb
148
176
  - lib/active_fulfillment/fulfillment/services/shipwire.rb
177
+ - lib/active_fulfillment/fulfillment/services/shopify_api.rb
149
178
  - lib/active_fulfillment/fulfillment/services/webgistix.rb
150
179
  - lib/active_fulfillment/fulfillment/version.rb
151
180
  - test/fixtures.yml
@@ -184,6 +213,7 @@ files:
184
213
  - test/unit/services/amazon_mws_test.rb
185
214
  - test/unit/services/amazon_test.rb
186
215
  - test/unit/services/shipwire_test.rb
216
+ - test/unit/services/shopify_api_test.rb
187
217
  - test/unit/services/webgistix_test.rb
188
218
  homepage: http://github.com/shopify/active_fulfillment
189
219
  licenses: []
@@ -215,6 +245,7 @@ test_files:
215
245
  - test/remote/webgistix_test.rb
216
246
  - test/remote/shipwire_test.rb
217
247
  - test/remote/amazon_mws_test.rb
248
+ - test/unit/services/shopify_api_test.rb
218
249
  - test/unit/services/amazon_test.rb
219
250
  - test/unit/services/webgistix_test.rb
220
251
  - test/unit/services/shipwire_test.rb
metadata.gz.sig CHANGED
Binary file