rafter-fulfillment 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
@@ -0,0 +1,50 @@
1
+ require 'cgi'
2
+
3
+ module Fulfillment
4
+ class Client
5
+
6
+ attr_accessor :verbose, :logger
7
+ attr_reader :api_key, :host, :base_uri, :scheme
8
+
9
+ DEFAULT_TIMEOUT = 10
10
+
11
+ def initialize(options = {})
12
+ client_options = HashWithIndifferentAccess.new(options)
13
+ @api_key = client_options[:api_key].nil? ? (raise ArgumentError.new(":api_key is a required argument")) : client_options[:api_key]
14
+ @host = client_options[:host].nil? ? (raise ArgumentError.new(":host is a required argument")) : client_options[:host]
15
+ @scheme = client_options[:scheme] || "https"
16
+ @base_uri = @scheme + "://" + @host
17
+ @verbose = client_options[:verbose] || false
18
+ @logger = client_options[:logger] || nil
19
+ @timeout = client_options[:timeout] || DEFAULT_TIMEOUT
20
+ end
21
+
22
+ def configure_http(http)
23
+ http.headers["X-API-KEY"] = @api_key
24
+ http.headers["Accept"] = Fulfillment::API_VERSION
25
+ http.headers["Content-Type"] = "application/json"
26
+ if scheme == "https"
27
+ http.use_ssl = Curl::CURL_USESSL_ALL
28
+ http.ssl_verify_peer = false
29
+ end
30
+ http.verbose = @verbose
31
+ unless @logger.nil?
32
+ http.on_debug { |type,data| @logger.info "Fulfillment Client #{data}" }
33
+ end
34
+ http.timeout = @timeout
35
+ end
36
+
37
+ def build_auth_url(resource_path)
38
+ @base_uri + resource_path
39
+ end
40
+
41
+ def add_query_parameter(curl, key, value)
42
+ current_url = curl.url
43
+ curl.url = current_url + (current_url.match(/\?/) ? "&" : "?") + "#{CGI::escape key.to_s}=#{CGI::escape value.to_s}"
44
+ end
45
+
46
+ def set_request_page(curl, page_num)
47
+ add_query_parameter(curl, "page", page_num)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,5 @@
1
+ module Fulfillment
2
+ class ClientException < StandardError
3
+
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Fulfillment
2
+ class CreationException < ClientException
3
+
4
+ end
5
+ end
@@ -0,0 +1,27 @@
1
+ module Fulfillment
2
+ class ModelBase
3
+
4
+ protected
5
+
6
+ def make_getter_methods(data_hash)
7
+ eigenclass = class << self; self; end
8
+
9
+ data_hash.each do |key, value|
10
+ instance_variable_set "@#{key}", value
11
+ eigenclass.send(:define_method, key) do
12
+ instance_variable_get "@#{key}"
13
+ end
14
+ end
15
+ end
16
+
17
+ def make_setter_methods(data_hash)
18
+ eigenclass = class << self; self; end
19
+
20
+ data_hash.each do |key, value|
21
+ eigenclass.send(:define_method, "@#{key}=") do |new_value|
22
+ instance_variable_set "@#{k}", new_value
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,169 @@
1
+ module Fulfillment
2
+ class Order < ModelBase
3
+
4
+ REJECT_CODE_BAD_ORDER_INFO = 1
5
+ REJECT_CODE_GENERIC = 2
6
+ REJECT_CODE_OUT_OF_STOCK = 3
7
+ REJECT_CODES = [REJECT_CODE_BAD_ORDER_INFO, REJECT_CODE_GENERIC, REJECT_CODE_OUT_OF_STOCK]
8
+
9
+ attr_accessor :client
10
+
11
+ def initialize(client, data)
12
+ @client = client
13
+ make_getter_methods(data)
14
+ end
15
+
16
+ def order_items(first_page_num = 1)
17
+ Fulfillment::OrderItem.list(self.client, self.public_id, first_page_num)
18
+ end
19
+
20
+ def process
21
+ Fulfillment::Order.processing_transition(self.client, self.public_id)
22
+ end
23
+
24
+ def processed
25
+ Fulfillment::Order.processed_transition(self.client, self.public_id)
26
+ end
27
+
28
+ def shipping
29
+ Fulfillment::Order.shipping_transition(self.client, self.public_id)
30
+ end
31
+
32
+ def shipped
33
+ Fulfillment::Order.shipped_transition(self.client, self.public_id)
34
+ end
35
+
36
+ def reject(rejected_code)
37
+ Fulfillment::Order.reject(self.client, self.public_id, rejected_code)
38
+ end
39
+
40
+ def create_shipment(shipment_hash)
41
+ Fulfillment::Shipment.create(self.client, self.public_id, shipment_hash)
42
+ end
43
+
44
+ def order_shipments
45
+ Fulfillment::Order.order_shipments(self.client, self.public_id)
46
+ end
47
+
48
+ class << self
49
+ def order_shipments(client, public_id, first_page_num = 1)
50
+ Fulfillment::PagedResult.construct(first_page_num) do |page_num|
51
+ curl = Curl::Easy.http_get(client.build_auth_url("/orders/#{public_id}/shipments")) do |curl|
52
+ client.configure_http(curl)
53
+ client.set_request_page(curl, page_num)
54
+ end
55
+
56
+ raise Fulfillment::ClientException.new("Could not load index of shipments for #{public_id}: \n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
57
+
58
+ shipment_hashes = JSON.parse(curl.body_str)
59
+ result = shipment_hashes.map { |sh| Fulfillment::Shipment.new(client, sh) }
60
+
61
+ Fulfillment::PagingEnvelope.envelop(curl, result)
62
+ end
63
+ end
64
+
65
+ def processing_transition(client, public_id)
66
+ curl = Curl::Easy.http_put(client.build_auth_url("/orders/#{public_id}/process"), {}) do |curl|
67
+ client.configure_http(curl)
68
+ end
69
+
70
+ raise Fulfillment::CreationException.new("Could not complete processing transition for #{public_id}:\n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
71
+
72
+ new(client, JSON.parse(curl.body_str))
73
+ end
74
+
75
+ def processed_transition(client, public_id)
76
+ curl = Curl::Easy.http_put(client.build_auth_url("/orders/#{public_id}/processed"), {}) do |curl|
77
+ client.configure_http(curl)
78
+ end
79
+
80
+ raise Fulfillment::CreationException.new("Could not complete processed transition for #{public_id}:\n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
81
+
82
+ new(client, JSON.parse(curl.body_str))
83
+ end
84
+
85
+ def shipping_transition(client, public_id)
86
+ curl = Curl::Easy.http_put(client.build_auth_url("/orders/#{public_id}/shipping"), {}.to_json) do |curl|
87
+ client.configure_http(curl)
88
+ end
89
+
90
+ raise Fulfillment::CreationException.new("Could not create shipped transition for #{public_id}:\n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
91
+
92
+ new(client, JSON.parse(curl.body_str))
93
+ end
94
+
95
+ def shipped_transition(client, public_id)
96
+ curl = Curl::Easy.http_put(client.build_auth_url("/orders/#{public_id}/shipped"), {}.to_json) do |curl|
97
+ client.configure_http(curl)
98
+ end
99
+
100
+ raise Fulfillment::CreationException.new("Could not create shipped transition for #{public_id}:\n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
101
+
102
+ new(client, JSON.parse(curl.body_str))
103
+ end
104
+
105
+ ##
106
+ # Reject the given FulfillmentOrder based on the public ID. The client must be the named
107
+ # FulfillmentProvider in order for the "rejection" to be successful.
108
+ def reject(client, public_id, rejected_code)
109
+ raise ArgumentError.new("Invalid Reject Code. The following are valid reject codes #{REJECT_CODES.join(",")}") unless REJECT_CODES.include?(rejected_code)
110
+ error_payload = {"rejected_code" => rejected_code}
111
+
112
+ curl = Curl::Easy.http_put(client.build_auth_url("/orders/#{public_id}/reject"), error_payload.to_json) do |curl|
113
+ client.configure_http(curl)
114
+ end
115
+
116
+ raise Fulfillment::CreationException.new("Could not reject order for #{public_id}:\n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
117
+
118
+ new(client, JSON.parse(curl.body_str))
119
+ end
120
+
121
+ def show(client, public_id)
122
+ curl = Curl::Easy.http_get(client.build_auth_url("/orders/#{public_id}")) do |curl|
123
+ client.configure_http(curl)
124
+ end
125
+
126
+ raise Fulfillment::ClientException.new("Could not get Order #{public_id}:\n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
127
+
128
+ new(client, JSON.parse(curl.body_str))
129
+ end
130
+
131
+ # return a collection of orders for fulfiller in ready status
132
+ def ready(client, first_page_num = 1)
133
+ Fulfillment::PagedResult.construct(first_page_num) do |page_num|
134
+ curl = Curl::Easy.new(client.build_auth_url("/orders/ready")) do |curl|
135
+ client.configure_http(curl)
136
+ client.set_request_page(curl, page_num)
137
+ end
138
+
139
+ curl.perform
140
+ ready_order_hashes = JSON.parse(curl.body_str)
141
+ result = ready_order_hashes.map { |roh| new(client, roh) }
142
+
143
+ Fulfillment::PagingEnvelope.envelop(curl, result)
144
+ end
145
+ end
146
+
147
+ def search(client, search_options = {})
148
+ first_page_num = 1
149
+ Fulfillment::PagedResult.construct(first_page_num) do |page_num|
150
+ curl = Curl::Easy.http_get(client.build_auth_url("/orders/search")) do |curl|
151
+ client.configure_http(curl)
152
+ client.set_request_page(curl, page_num)
153
+ search_options.each { |k, v| client.add_query_parameter(curl, k, v) }
154
+ curl
155
+ end
156
+
157
+ raise Fulfillment::SearchException.new("Could not search orders with search options #{search_options}:\n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
158
+
159
+ search_result_order_array = JSON.parse(curl.body_str)
160
+ orders = []
161
+ search_result_order_array.each { |oh| orders << new(client, oh) }
162
+
163
+ Fulfillment::PagingEnvelope.envelop(curl, orders)
164
+ end
165
+ end
166
+
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,118 @@
1
+ module Fulfillment
2
+ class OrderItem < ModelBase
3
+
4
+ attr_accessor :client, :order_public_id
5
+
6
+ GENERIC_REJECTION = 1 # un-categorized rejection reason
7
+ OUT_OF_STOCK_REJECTION = 2 # fulfillment provider is temporarily out of stock
8
+ ITEM_NOT_STOCKED_REJECTION = 3 # fulfillment provider does not know about this item at all
9
+ RESERVE_EXHAUSTED_REJECTION = 4
10
+ INVALID_QUANTITY_REJECTION = 5
11
+ REJECT_CODES = [GENERIC_REJECTION, OUT_OF_STOCK_REJECTION,
12
+ ITEM_NOT_STOCKED_REJECTION, RESERVE_EXHAUSTED_REJECTION,
13
+ INVALID_QUANTITY_REJECTION]
14
+
15
+ def initialize(client, data)
16
+ @client = client
17
+ make_getter_methods(data)
18
+ end
19
+
20
+ def reject(rejected_code)
21
+ Fulfillment::OrderItem.reject(self.client, self.order_public_id, self.public_id, rejected_code)
22
+ end
23
+
24
+ def acknowledge(acknowledgements)
25
+ Fulfillment::OrderItem.acknowledge(self.client, self.order_public_id, self.public_id, acknowledgements)
26
+ end
27
+
28
+ def process
29
+ Fulfillment::OrderItem.process(self.client, self.order_public_id, self.public_id)
30
+ end
31
+
32
+ class << self
33
+
34
+ ##
35
+ # Acknowledge quantites accepted / rejected of the given FulfillmentOrderItem based on the FulfillmentOrder public ID
36
+ # and the FulfillmentOrderItem public ID.
37
+ # Accepted and Rejected quantities must be present in the acknowledgements hash
38
+ def acknowledge(client, order_public_id, order_item_public_id, acknowledgements)
39
+ if acknowledgements["quantity_accepted"].nil? || acknowledgements["quantity_rejected"].nil?
40
+ raise ArgumentError.new("Accepted and Rejected quantities must be present in the acknowledgements hash.")
41
+ end
42
+
43
+ curl = Curl::Easy.http_put(client.build_auth_url("/orders/#{order_public_id}/items/#{order_item_public_id}/acknowledge"), acknowledgements.to_json) do |curl|
44
+ client.configure_http(curl)
45
+ end
46
+
47
+ if curl.response_code != 200
48
+ raise Fulfillment::CreationException.new("Could not acknowledge item #{order_item_public_id} from order #{order_public_id}:\n\n Response Body:\n #{curl.body_str}")
49
+ end
50
+
51
+ new(client, JSON.parse(curl.body_str))
52
+ end
53
+
54
+ ##
55
+ # Reject the given FulfillmentOrderItem based on the FulfillmentOrder public ID and the FulfillmentOrderItem
56
+ # public ID. The client must be the named FulfillmentProvider in order for the "rejection" to be successful.
57
+ def reject(client, order_public_id, order_item_public_id, rejected_code)
58
+ raise ArgumentError.new("Invalid Reject Code. The following are valid reject codes #{REJECT_CODES.join(",")}") unless REJECT_CODES.include?(rejected_code)
59
+ error_payload = {"rejected_code" => rejected_code}
60
+
61
+ curl = Curl::Easy.http_put(client.build_auth_url("/orders/#{order_public_id}/items/#{order_item_public_id}/reject"), error_payload.to_json) do |curl|
62
+ client.configure_http(curl)
63
+ end
64
+
65
+ raise Fulfillment::CreationException.new("Could not reject item #{order_item_public_id} from order #{order_public_id}:\n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
66
+
67
+ new(client, JSON.parse(curl.body_str))
68
+ end
69
+
70
+ ##
71
+ # Process a given FulfillmentOrderItem based on the FulfillmentOrder public_id and the FulfillmentOrderItem
72
+ # public_id. The client must be the named FulfillmentProvider in order for the 'process' to be successful.
73
+ def process(client, order_public_id, order_item_public_id)
74
+ curl = Curl::Easy.http_put(client.build_auth_url("/orders/#{order_public_id}/items/#{order_item_public_id}/process"), {}) do |curl|
75
+ client.configure_http(curl)
76
+ end
77
+
78
+ raise Fulfillment::CreationException.new("Could not process item #{order_item_public_id} from order #{order_public_id}:\n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
79
+
80
+ new(client, JSON.parse(curl.body_str))
81
+ end
82
+
83
+ def list(client, order_public_id, first_page_num = 1)
84
+ Fulfillment::PagedResult.construct(first_page_num) do |page_num|
85
+ curl = Curl::Easy.http_get(client.build_auth_url("/orders/#{order_public_id}/items")) do |curl|
86
+ client.configure_http(curl)
87
+ client.set_request_page(curl, page_num)
88
+ end
89
+
90
+ raise Fulfillment::ClientException.new("Could not load index of items for order #{order_public_id}: \n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
91
+
92
+ order_item_result_array = JSON.parse(curl.body_str)
93
+ order_items = []
94
+ order_item_result_array.each do |ira|
95
+ order_item = new(client, ira)
96
+ order_item.order_public_id = order_public_id
97
+ order_items << order_item
98
+ end
99
+
100
+ Fulfillment::PagingEnvelope.envelop(curl, order_items)
101
+ end
102
+ end
103
+
104
+ def show(client, order_public_id, order_item_public_id)
105
+ curl = Curl::Easy.http_get(client.build_auth_url("/orders/#{order_public_id}/items/#{order_item_public_id}")) do |curl|
106
+ client.configure_http(curl)
107
+ end
108
+
109
+ raise Fulfillment::ClientException.new("Could not get order item #{order_item_public_id} for order #{order_public_id}:\n\n Response Body:\n #{curl.body_str}") unless curl.response_code == 200
110
+
111
+ order_item = new(client, JSON.parse(curl.body_str))
112
+ order_item.order_public_id = order_public_id
113
+ order_item
114
+ end
115
+
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,45 @@
1
+ module Fulfillment
2
+ class PagedResult
3
+ include Enumerable
4
+
5
+ attr_reader :first_page_num
6
+
7
+ # Create a
8
+
9
+ # *first_page_number* is the starting page for the associated API call.
10
+ # *api_caller_proc* is a Proc that returns a hash (of the type returned
11
+ # by Exchange::PagingEnvelope.envelope). Calling the proc and passing
12
+ # it a page_number should return an enveloped API call result.
13
+ def initialize(first_page_num, api_caller_proc)
14
+ @first_page_num = first_page_num
15
+ @api_caller_proc = api_caller_proc
16
+ end
17
+
18
+ def pages
19
+ entries
20
+ end
21
+
22
+ def results
23
+ pages.flatten
24
+ end
25
+
26
+ def each
27
+ page_num = @first_page_num
28
+ enveloped_page = @api_caller_proc.call(page_num)
29
+ yield enveloped_page[:data]
30
+ while (page_num < enveloped_page[:total_pages])
31
+ page_num += 1
32
+ enveloped_page = @api_caller_proc.call(page_num)
33
+ yield enveloped_page[:data]
34
+ end
35
+ end
36
+
37
+ class << self
38
+
39
+ # Alternative constructor that takes a block rather than a Proc instance
40
+ def construct(first_page_num, &api_caller_proc)
41
+ new(first_page_num, api_caller_proc)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,36 @@
1
+ module Fulfillment
2
+ module PagingEnvelope
3
+ class << self
4
+
5
+ # Returns a hash like {per_page:100, total_pages:5, data: data}
6
+ def envelop(curl, data)
7
+ per_page, total_pages = get_pages_from_curl(curl)
8
+ {per_page: per_page, total_pages: total_pages, data: data}
9
+ end
10
+
11
+ private
12
+
13
+ def get_paging_json_from_response_header(response_header)
14
+ if response_header.is_a? String
15
+ get_paging_json_from_response_header(response_header.split)
16
+ elsif response_header.count == 0
17
+ nil
18
+ elsif response_header[0].upcase == "X-API-PAGINATION:"
19
+ response_header[1]
20
+ else
21
+ get_paging_json_from_response_header(response_header[1..-1])
22
+ end
23
+ end
24
+
25
+ def get_pages_from_curl(curl_response)
26
+ response_header = curl_response.header_str
27
+ if (paging_json = get_paging_json_from_response_header(response_header))
28
+ paging_hash = JSON.parse(paging_json)
29
+ [paging_hash["per_page"], paging_hash["total_pages"]]
30
+ else
31
+ [nil, nil]
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end