rafter-fulfillment 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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