active_fulfillment 2.1.6 → 2.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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