solidus_fulfillment 2.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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/LICENSE +27 -0
- data/README.md +74 -0
- data/Rakefile +15 -0
- data/Versionfile +4 -0
- data/app/models/spree/fulfillment.rb +134 -0
- data/app/models/spree/shipment_decorator.rb +87 -0
- data/lib/generators/solidus/fulfillment/install/install_generator.rb +8 -0
- data/lib/solidus/fulfillment/amazon_fulfillment.rb +189 -0
- data/lib/solidus/fulfillment/engine.rb +14 -0
- data/lib/solidus_fulfillment.rb +2 -0
- data/lib/tasks/solidus_fulfillment.rake +24 -0
- data/script/rails +7 -0
- data/solidus_fulfillment.gemspec +30 -0
- data/spec/spec_helper.rb +46 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8cfed147ac17cacd2145fdcf48a2ceb3c3c227a8
|
4
|
+
data.tar.gz: 4f21644a4f060d33977c0e9a68ded129b77e366c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d2bfcbcef93c29bd6f451cd80273447c449e4e35c3d8bc857da22adc32a43e15c22c3ace44a5cb771879ebac48dcac58380b8d3a824e1319af1252af074cd9e0
|
7
|
+
data.tar.gz: ce90938443ba6f680edf947ee85bbd8737e032d2b805d62ef5db1d6ac3604605acefcdea4bd8488124ef3042d331ce5fc8d1702e7113bebf89a9e8a300ed2ae6
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Copyright (c) 2017 Rémy Coutable, released under the New BSD License
|
2
|
+
Copyright (c) 2011 WIMM Labs, released under the New BSD License
|
3
|
+
All rights reserved.
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without modification,
|
6
|
+
are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
* Redistributions of source code must retain the above copyright notice,
|
9
|
+
this list of conditions and the following disclaimer.
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
12
|
+
and/or other materials provided with the distribution.
|
13
|
+
* Neither the name of the Rails Dog LLC nor the names of its
|
14
|
+
contributors may be used to endorse or promote products derived from this
|
15
|
+
software without specific prior written permission.
|
16
|
+
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
18
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
19
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
20
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
21
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
22
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
23
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
24
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
25
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
26
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
27
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
SolidusFulfillment is a solidus extension to do fulfillment processing via
|
2
|
+
various fulfillment services when a shipment becomes ready.
|
3
|
+
|
4
|
+
The extension adds an additional state to the Shipment state machine called
|
5
|
+
`fulfilling` which acts as the transition between `ready` and `shipped`.
|
6
|
+
|
7
|
+
When a shipment becomes `ready` it is eligible for fulfillment:
|
8
|
+
|
9
|
+
1. A `solidus_fulfillment:process` rake task intended to be called from a cron
|
10
|
+
job checks for `ready` shipments (by delegating to the
|
11
|
+
`solidus_fulfillment:process:ready` task) and initiates the fulfillment via
|
12
|
+
the merchant API.
|
13
|
+
1. If the fulfillment transaction succeeds, the shipment enters the `fulfilling`
|
14
|
+
state.
|
15
|
+
1. The `solidus_fulfillment:process:fulfilling` rake task then queries the
|
16
|
+
merchant's API for tracking numbers of any orders that are being fulfilled.
|
17
|
+
1. If the tracking numbers are found, the shipment transitions into the
|
18
|
+
`shipped` state and an email is sent to the customer.
|
19
|
+
|
20
|
+
Stock levels can also be updated with the
|
21
|
+
`solidus_fulfillment:process:stock_levels` rake task which is intended to be
|
22
|
+
called from a cron job.
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
### Add to your gemfile:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem 'whenever', require: false # if you want whenever to manage the cron job
|
30
|
+
gem 'solidus_fulfillment'
|
31
|
+
```
|
32
|
+
|
33
|
+
### Create config/fulfillment.yml:
|
34
|
+
|
35
|
+
```yml
|
36
|
+
development:
|
37
|
+
adapter: amazon
|
38
|
+
api_key: <YOUR AMAZON AWS API KEY>
|
39
|
+
secret_key: <YOUR AMAZON AWS SECRET KEY>
|
40
|
+
development_mode: true
|
41
|
+
|
42
|
+
test:
|
43
|
+
adapter: amazon
|
44
|
+
api_key: <YOUR AMAZON AWS API KEY>
|
45
|
+
secret_key: <YOUR AMAZON AWS SECRET KEY>
|
46
|
+
|
47
|
+
production:
|
48
|
+
adapter: amazon
|
49
|
+
api_key: <YOUR AMAZON AWS API KEY>
|
50
|
+
secret_key: <YOUR AMAZON AWS SECRET KEY>
|
51
|
+
```
|
52
|
+
|
53
|
+
### Create config/schedule.rb:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
every :hour do
|
57
|
+
rake "solidus_fulfillment:process"
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
### Add to deploy.rb:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
require 'whenever/capistrano' # if you want whenever to manage the cron job
|
65
|
+
```
|
66
|
+
|
67
|
+
### Configure the store
|
68
|
+
|
69
|
+
Set the SKU code for your products to be equal to the Amazon fulfillment SKU code.
|
70
|
+
|
71
|
+
----
|
72
|
+
|
73
|
+
Copyright (c) 2017 Rémy Coutable, released under the New BSD License
|
74
|
+
Copyright (c) 2011 WIMM Labs, released under the New BSD License
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'spree/testing_support/common_rake'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new
|
8
|
+
|
9
|
+
task :default => [:spec]
|
10
|
+
|
11
|
+
desc 'Generates a dummy app for testing'
|
12
|
+
task :test_app do
|
13
|
+
ENV['LIB_NAME'] = 'spree_fulfillment'
|
14
|
+
Rake::Task['common:test_app'].invoke
|
15
|
+
end
|
data/Versionfile
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
module Spree
|
2
|
+
class Fulfillment
|
3
|
+
CONFIG_FILE = Rails.root.join('config/fulfillment.yml')
|
4
|
+
CONFIG = HashWithIndifferentAccess.new(YAML.load_file(CONFIG_FILE)[Rails.env])
|
5
|
+
|
6
|
+
TrackingInfo = Struct.new(:carrier, :tracking_number, :ship_time) do
|
7
|
+
def to_hash
|
8
|
+
{
|
9
|
+
carrier: carrier,
|
10
|
+
tracking_number: tracking_number,
|
11
|
+
ship_time: ship_time
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.service(shipment = nil)
|
17
|
+
('Solidus::Fulfillment::' + "#{adapter}_fulfillment".camelize).constantize.new(shipment)
|
18
|
+
rescue NameError
|
19
|
+
require "solidus/fulfillment/#{adapter}_fulfillment"
|
20
|
+
retry
|
21
|
+
rescue LoadError
|
22
|
+
log "Spree::Fulfillment.service: cannot load #{'Solidus::Fulfillment::' + "#{adapter}_fulfillment".camelize}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.adapter
|
26
|
+
return @adapter if defined?(@adapter)
|
27
|
+
|
28
|
+
@adapter = config[:adapter]
|
29
|
+
|
30
|
+
unless @adapter
|
31
|
+
raise "Missing adapter for #{Rails.env} -- Check config/fulfillment.yml"
|
32
|
+
end
|
33
|
+
|
34
|
+
@adapter
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.fulfill(shipment)
|
38
|
+
service(shipment).fulfill
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.config
|
42
|
+
CONFIG
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.log(msg)
|
46
|
+
Rails.logger.info "**** solidus_fulfillment: #{msg}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Passes any shipments that are ready to the fulfillment service
|
50
|
+
def self.process_ready
|
51
|
+
log 'Spree::Fulfillment.process_ready start'
|
52
|
+
|
53
|
+
Spree::Shipment.ready.ids.each do |shipment_id|
|
54
|
+
shipment = Spree::Shipment.find(shipment_id)
|
55
|
+
|
56
|
+
next unless shipment && shipment.ready?
|
57
|
+
|
58
|
+
log "Request to ship shipment ##{shipment.id}"
|
59
|
+
begin
|
60
|
+
shipment.ship!
|
61
|
+
rescue => ex
|
62
|
+
log "Spree::Fulfillment.process_ready: Failed to ship id #{shipment.id} due to #{ex}"
|
63
|
+
Airbrake.notify(e) if defined?(Airbrake)
|
64
|
+
# continue on and try other shipments so that one bad shipment doesn't
|
65
|
+
# block an entire queue
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Gets tracking number and sends ship email when fulfillment house is done
|
71
|
+
def self.process_fulfilling
|
72
|
+
log 'Spree::Fulfillment.process_fulfilling start'
|
73
|
+
|
74
|
+
Spree::Shipment.fulfilling.each do |shipment|
|
75
|
+
next if shipment.shipped?
|
76
|
+
|
77
|
+
tracking_info = remote_tracking_info(shipment)
|
78
|
+
log "Spree::Fulfillment.process_fulfilling: tracking_info #{tracking_info}"
|
79
|
+
next unless tracking_info
|
80
|
+
|
81
|
+
if tracking_info == :error
|
82
|
+
log 'Spree::Fulfillment.process_fulfilling: Could not retrieve' \
|
83
|
+
"tracking information for shipment #{shipment.id} (order ID: "\
|
84
|
+
"#{shipment.number})"
|
85
|
+
shipment.cancel
|
86
|
+
else
|
87
|
+
log 'Spree::Fulfillment.process_fulfilling: Tracking information: ' \
|
88
|
+
"#{tracking_info.inspect}"
|
89
|
+
shipment.attributes = {
|
90
|
+
shipped_at: tracking_info.ship_time,
|
91
|
+
tracking: "#{tracking_info.carrier}::#{tracking_info.tracking_number}"
|
92
|
+
}
|
93
|
+
shipment.ship!
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.process_stock_levels
|
99
|
+
log 'Spree::Fulfillment.process_stock_levels start'
|
100
|
+
|
101
|
+
skus = Spree::Variant.pluck(:sku)
|
102
|
+
default_stock_location = Spree::StockLocation.find_by(name: 'default')
|
103
|
+
|
104
|
+
response = service.fetch_stock_levels(skus)
|
105
|
+
|
106
|
+
response.params['stock_levels'].each do |sku, stock|
|
107
|
+
variant = Spree::Variant.find_by(sku: sku)
|
108
|
+
variant.stock_items.
|
109
|
+
find_by(stock_location_id: default_stock_location.id).
|
110
|
+
set_count_on_hand(stock)
|
111
|
+
log "Spree::Fulfillment.process_stock_levels: variant #{variant.inspect} has a new stock level of #{stock}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.remote_tracking_info(shipment)
|
116
|
+
response = service(shipment).fetch_tracking_data
|
117
|
+
return unless response
|
118
|
+
|
119
|
+
tracking_info = TrackingInfo.new(
|
120
|
+
response.params.dig('tracking_companies', shipment.number.to_s)&.first,
|
121
|
+
response.params.dig('tracking_numbers', shipment.number.to_s)&.first,
|
122
|
+
response.params.dig('shipping_date_times', shipment.number.to_s)&.first
|
123
|
+
)
|
124
|
+
|
125
|
+
unless tracking_info.carrier &&
|
126
|
+
tracking_info.tracking_number &&
|
127
|
+
tracking_info.ship_time
|
128
|
+
return :error
|
129
|
+
end
|
130
|
+
|
131
|
+
tracking_info.to_hash
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
Spree::Shipment.class_eval do
|
2
|
+
scope :fulfilling, -> { with_state('fulfilling') }
|
3
|
+
scope :fulfill_failed, -> { with_state('fulfill_failed') }
|
4
|
+
|
5
|
+
state_machines[:state] = nil # reset original state machine to start from scratch.
|
6
|
+
|
7
|
+
# This is a modified version of the original spree shipment state machine
|
8
|
+
# with the indicated changes.
|
9
|
+
state_machine initial: :pending, use_transactions: false do
|
10
|
+
event :ready do
|
11
|
+
transition from: :pending, to: :shipped, if: :can_transition_from_pending_to_shipped?
|
12
|
+
transition from: :pending, to: :ready, if: :can_transition_from_pending_to_ready?
|
13
|
+
end
|
14
|
+
|
15
|
+
event :pend do
|
16
|
+
transition from: :ready, to: :pending
|
17
|
+
end
|
18
|
+
|
19
|
+
event :ship do
|
20
|
+
transition from: [:ready, :canceled], to: :fulfilling # was to: :shipped
|
21
|
+
# new transition
|
22
|
+
transition from: :fulfilling, to: :shipped
|
23
|
+
end
|
24
|
+
after_transition to: :shipped, do: :after_ship
|
25
|
+
|
26
|
+
# new callback
|
27
|
+
before_transition to: :fulfilling, do: :before_fulfilling
|
28
|
+
|
29
|
+
event :cancel do
|
30
|
+
transition to: :canceled, from: [:pending, :ready]
|
31
|
+
# new transition
|
32
|
+
transition from: :fulfilling, to: :fulfill_failed
|
33
|
+
end
|
34
|
+
after_transition to: :canceled, do: :after_cancel
|
35
|
+
|
36
|
+
event :resume do
|
37
|
+
transition from: :canceled, to: :ready, if: :can_transition_from_canceled_to_ready?
|
38
|
+
transition from: :canceled, to: :pending
|
39
|
+
end
|
40
|
+
after_transition from: :canceled, to: [:pending, :ready, :shipped], do: :after_resume
|
41
|
+
|
42
|
+
after_transition do |shipment, transition|
|
43
|
+
shipment.state_changes.create!(
|
44
|
+
previous_state: transition.from,
|
45
|
+
next_state: transition.to,
|
46
|
+
name: 'shipment'
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# If there's an error submitting to the fulfillment service, we should halt
|
52
|
+
# the transition to 'fulfill' and stay in 'ready'. That way transient errors
|
53
|
+
# will get rehandled. If there are persistent errors, that should be treated
|
54
|
+
# as a bug.
|
55
|
+
def before_fulfilling
|
56
|
+
response = Spree::Fulfillment.fulfill(self) # throws :halt on error, which aborts transition
|
57
|
+
|
58
|
+
# Stop the transition to shipped if there was an error.
|
59
|
+
unless response.success?
|
60
|
+
if Spree::Fulfillment.config[:development_mode] &&
|
61
|
+
response.params['faultstring'] =~ /the SellerSKU for Item Id: \S+ is invalid/
|
62
|
+
Spree::Fulfillment.log 'Ignoring missing catalog item (test / dev setting - should not see this on prod)'
|
63
|
+
else
|
64
|
+
Spree::Fulfillment.log 'Abort - response was in error'
|
65
|
+
throw :halt
|
66
|
+
end
|
67
|
+
end
|
68
|
+
# TODO: Narrow down the catched exception
|
69
|
+
rescue => ex
|
70
|
+
Spree::Fulfillment.log "Spree::Shipment#before_fulfilling failed: #{ex.message}" \
|
71
|
+
"\n#{ex.backtrace}"
|
72
|
+
throw :halt
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_method :orig_determine_state, :determine_state
|
76
|
+
# Determines the appropriate +state+ according to the following logic:
|
77
|
+
#
|
78
|
+
# canceled if order is canceled
|
79
|
+
# pending unless order is complete and +order.payment_state+ is +paid+
|
80
|
+
# shipped if already shipped (ie. does not change the state)
|
81
|
+
# ready all other cases
|
82
|
+
def determine_state(order)
|
83
|
+
return state if ['fulfilling', 'fulfill_failed', 'shipped'].include?(state)
|
84
|
+
|
85
|
+
orig_determine_state(order)
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'active_fulfillment'
|
2
|
+
|
3
|
+
ActiveFulfillment::Service.logger = Rails.logger
|
4
|
+
ActiveFulfillment::AmazonMarketplaceWebService.class_eval do
|
5
|
+
# Monkeypatch of the original parse_tracking_response to include the shipping date.
|
6
|
+
# Changed lines are marked.
|
7
|
+
def parse_tracking_response(document)
|
8
|
+
response = {
|
9
|
+
tracking_numbers: {},
|
10
|
+
tracking_companies: {},
|
11
|
+
tracking_urls: {},
|
12
|
+
shipping_date_times: {} # Additional key
|
13
|
+
}
|
14
|
+
|
15
|
+
tracking_numbers = document.css('FulfillmentShipmentPackage > member > TrackingNumber'.freeze)
|
16
|
+
if tracking_numbers.present?
|
17
|
+
order_id = document.at_css('FulfillmentOrder > SellerFulfillmentOrderId'.freeze).text.strip
|
18
|
+
response[:tracking_numbers][order_id] = tracking_numbers.map{ |t| t.text.strip }
|
19
|
+
end
|
20
|
+
|
21
|
+
tracking_companies = document.css('FulfillmentShipmentPackage > member > CarrierCode'.freeze)
|
22
|
+
if tracking_companies.present?
|
23
|
+
order_id = document.at_css('FulfillmentOrder > SellerFulfillmentOrderId'.freeze).text.strip
|
24
|
+
response[:tracking_companies][order_id] = tracking_companies.map{ |t| t.text.strip }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Changes start here
|
28
|
+
shipping_date_times = document.css('FulfillmentShipment > member > ShippingDateTime'.freeze)
|
29
|
+
if shipping_date_times.present?
|
30
|
+
response[:shipping_date_times][order_id] = shipping_date_times.map { |t| t.text.strip }
|
31
|
+
end
|
32
|
+
# Changes end here
|
33
|
+
|
34
|
+
response[:response_status] = SUCCESS
|
35
|
+
|
36
|
+
Response.new(success?(response), message_from(response), response)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Solidus
|
41
|
+
module Fulfillment
|
42
|
+
class AmazonFulfillment
|
43
|
+
def initialize(shipment = nil)
|
44
|
+
@shipment = shipment
|
45
|
+
end
|
46
|
+
|
47
|
+
# Runs inside a state_machine callback. So throwing :halt is how we abort things.
|
48
|
+
def fulfill
|
49
|
+
sleep 1 # avoid throttle from Amazon
|
50
|
+
|
51
|
+
response = remote.fulfill(order_id, address, line_items, options)
|
52
|
+
Spree::Fulfillment.log "Spree::AmazonFulfillment#fulfill: order_id: " \
|
53
|
+
"#{order_id}\naddress: #{address}\nline_items: #{line_items}\noptions: " \
|
54
|
+
"#{options}\nresponse: #{response.params}"
|
55
|
+
|
56
|
+
response
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the tracking number if there is one, else :error if there's a
|
60
|
+
# problem with the shipment that will result in a permanent failure to
|
61
|
+
# fulfill, else nil.
|
62
|
+
def fetch_tracking_data
|
63
|
+
sleep 1 # avoid throttle from Amazon
|
64
|
+
|
65
|
+
response = begin
|
66
|
+
remote.fetch_tracking_data([order_id])
|
67
|
+
rescue => ex
|
68
|
+
Spree::Fulfillment.log 'Spree::AmazonFulfillment#fetch_tracking_data: Failed to get ' \
|
69
|
+
"tracking info for shipment #{@shipment.id} (order ID: #{order_id})"
|
70
|
+
Spree::Fulfillment.log "Spree::AmazonFulfillment#fetch_tracking_data: #{ex}"
|
71
|
+
Airbrake.notify(e) if defined?(Airbrake)
|
72
|
+
|
73
|
+
return nil
|
74
|
+
end
|
75
|
+
|
76
|
+
Spree::Fulfillment.log "Spree::AmazonFulfillment#fetch_tracking_data: #{response.params}"
|
77
|
+
|
78
|
+
response
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the stock levels for the given skus
|
82
|
+
def fetch_stock_levels(skus)
|
83
|
+
sleep 1 # avoid throttle from Amazon
|
84
|
+
|
85
|
+
response = begin
|
86
|
+
remote.fetch_stock_levels(skus: skus)
|
87
|
+
rescue => ex
|
88
|
+
Spree::Fulfillment.log 'Spree::AmazonFulfillment#fetch_stock_levels: Failed to get ' \
|
89
|
+
"stock levels"
|
90
|
+
Spree::Fulfillment.log "Spree::AmazonFulfillment#fetch_stock_levels: #{ex}"
|
91
|
+
Airbrake.notify(e) if defined?(Airbrake)
|
92
|
+
|
93
|
+
return nil
|
94
|
+
end
|
95
|
+
|
96
|
+
Spree::Fulfillment.log "Spree::AmazonFulfillment#fetch_stock_levels: #{response.params}"
|
97
|
+
|
98
|
+
response
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# For Amazon these are the API access key and secret.
|
104
|
+
def credentials
|
105
|
+
@credentials ||= {
|
106
|
+
login: Spree::Fulfillment.config[:api_key],
|
107
|
+
password: Spree::Fulfillment.config[:secret_key],
|
108
|
+
seller_id: Spree::Fulfillment.config[:seller_id]
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def remote
|
113
|
+
@remote ||= ActiveFulfillment::Base.service('amazon_marketplace_web').new(credentials)
|
114
|
+
end
|
115
|
+
|
116
|
+
def order_id
|
117
|
+
@order_id ||= @shipment.number
|
118
|
+
end
|
119
|
+
|
120
|
+
def address
|
121
|
+
@address ||= begin
|
122
|
+
ship_address = @shipment.order.ship_address
|
123
|
+
|
124
|
+
{
|
125
|
+
name: "#{ship_address.firstname} #{ship_address.lastname}",
|
126
|
+
address1: ship_address.address1,
|
127
|
+
address2: ship_address.address2,
|
128
|
+
city: ship_address.city,
|
129
|
+
state: ship_address.state.abbr,
|
130
|
+
country: ship_address.state.country.iso,
|
131
|
+
zip: ship_address.zipcode
|
132
|
+
}
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def max_quantity_failsafe(quantity)
|
137
|
+
return quantity unless Spree::Fulfillment.config[:max_quantity_failsafe]
|
138
|
+
|
139
|
+
[Spree::Fulfillment.config[:max_quantity_failsafe], quantity].min
|
140
|
+
end
|
141
|
+
|
142
|
+
def line_items
|
143
|
+
@line_items ||= begin
|
144
|
+
skus = @shipment.inventory_units.map do |inventory_unit|
|
145
|
+
sku = inventory_unit.variant.sku
|
146
|
+
raise "Missing sku for #{inventory_unit.variant}" if sku.blank?
|
147
|
+
sku
|
148
|
+
end.uniq
|
149
|
+
|
150
|
+
skus.map do |sku|
|
151
|
+
quantity = @shipment.inventory_units.select do |inventory_unit|
|
152
|
+
inventory_unit.variant.sku == sku
|
153
|
+
end.size
|
154
|
+
|
155
|
+
{
|
156
|
+
sku: sku,
|
157
|
+
quantity: max_quantity_failsafe(quantity)
|
158
|
+
}
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def shipping_method
|
164
|
+
return @shipping_method if defined?(@shipping_method)
|
165
|
+
|
166
|
+
raw_shipping_method = @shipment.shipping_method
|
167
|
+
@shipping_method = 'Standard' unless raw_shipping_method
|
168
|
+
@shipping_method ||=
|
169
|
+
case raw_shipping_method.name.downcase
|
170
|
+
when /expedited/
|
171
|
+
'Expedited'
|
172
|
+
when /priority/
|
173
|
+
'Priority'
|
174
|
+
else
|
175
|
+
'Standard'
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def options
|
180
|
+
@options ||= {
|
181
|
+
shipping_method: shipping_method,
|
182
|
+
order_date: @shipment.order.created_at,
|
183
|
+
comment: 'Thank you for your order.',
|
184
|
+
email: @shipment.order.email
|
185
|
+
}
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Solidus
|
2
|
+
module Fulfillment
|
3
|
+
class Engine < Rails::Engine
|
4
|
+
isolate_namespace Spree
|
5
|
+
engine_name 'solidus_fulfillment'
|
6
|
+
|
7
|
+
config.to_prepare do
|
8
|
+
Dir.glob(File.join(__dir__, '../../../app/**/*_decorator*.rb')) do |c|
|
9
|
+
require_dependency(c)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
namespace :solidus_fulfillment do
|
2
|
+
desc "Handles shipments that are ready for or have completed fulfillment"
|
3
|
+
task process: :environment do
|
4
|
+
Rake::Task['solidus_fulfillment:process:ready'].invoke
|
5
|
+
Rake::Task['solidus_fulfillment:process:fulfilling'].invoke
|
6
|
+
end
|
7
|
+
|
8
|
+
namespace :process do
|
9
|
+
desc "Passes any shipments that are ready to the fulfillment service"
|
10
|
+
task ready: :environment do
|
11
|
+
Spree::Fulfillment.process_ready
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Gets tracking number and sends ship email when fulfillment house is done"
|
15
|
+
task fulfilling: :environment do
|
16
|
+
Spree::Fulfillment.process_fulfilling
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "Updates the stock levels"
|
20
|
+
task stock_levels: :environment do
|
21
|
+
Spree::Fulfillment.process_stock_levels
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/script/rails
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
|
2
|
+
|
3
|
+
ENGINE_ROOT = File.expand_path('../..', __FILE__)
|
4
|
+
ENGINE_PATH = File.expand_path('../../lib/spree_fulfillment/engine', __FILE__)
|
5
|
+
|
6
|
+
require 'rails/all'
|
7
|
+
require 'rails/engine/commands'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.platform = Gem::Platform::RUBY
|
4
|
+
s.name = 'solidus_fulfillment'
|
5
|
+
s.version = '2.0.0'
|
6
|
+
s.summary = 'Solidus extension to do fulfillment processing via various services when a shipment becomes ready'
|
7
|
+
s.description = 'Solidus extension to do fulfillment processing via various services when a shipment becomes ready'
|
8
|
+
|
9
|
+
s.required_ruby_version = '>= 2.2.7'
|
10
|
+
|
11
|
+
s.author = 'Rémy Coutable'
|
12
|
+
s.email = 'remy@rymai.me'
|
13
|
+
s.homepage = 'https://rubygems.org/gems/solidus_fulfillment'
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.require_path = 'lib'
|
18
|
+
s.requirements << 'none'
|
19
|
+
|
20
|
+
s.add_dependency 'solidus_core', '~> 2.1'
|
21
|
+
s.add_dependency 'active_fulfillment'
|
22
|
+
|
23
|
+
# s.add_development_dependency 'capybara', '~> 1.1.2'
|
24
|
+
# s.add_development_dependency 'coffee-rails'
|
25
|
+
# s.add_development_dependency 'factory_girl', '~> 2.6.4'
|
26
|
+
# s.add_development_dependency 'ffaker'
|
27
|
+
# s.add_development_dependency 'rspec-rails', '~> 2.9'
|
28
|
+
# s.add_development_dependency 'sass-rails'
|
29
|
+
# s.add_development_dependency 'sqlite3'
|
30
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# Configure Rails Environment
|
2
|
+
ENV['RAILS_ENV'] = 'test'
|
3
|
+
|
4
|
+
require File.expand_path('../dummy/config/environment.rb', __FILE__)
|
5
|
+
|
6
|
+
require 'rspec/rails'
|
7
|
+
require 'ffaker'
|
8
|
+
|
9
|
+
# Requires supporting ruby files with custom matchers and macros, etc,
|
10
|
+
# in spec/support/ and its subdirectories.
|
11
|
+
Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f }
|
12
|
+
|
13
|
+
# Requires factories defined in spree_core
|
14
|
+
require 'spree/core/testing_support/factories'
|
15
|
+
require 'spree/core/testing_support/controller_requests'
|
16
|
+
require 'spree/core/testing_support/authorization_helpers'
|
17
|
+
require 'spree/core/url_helpers'
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
config.include FactoryGirl::Syntax::Methods
|
21
|
+
|
22
|
+
# == URL Helpers
|
23
|
+
#
|
24
|
+
# Allows access to Spree's routes in specs:
|
25
|
+
#
|
26
|
+
# visit spree.admin_path
|
27
|
+
# current_path.should eql(spree.products_path)
|
28
|
+
config.include Spree::Core::UrlHelpers
|
29
|
+
|
30
|
+
# == Mock Framework
|
31
|
+
#
|
32
|
+
# If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
|
33
|
+
#
|
34
|
+
# config.mock_with :mocha
|
35
|
+
# config.mock_with :flexmock
|
36
|
+
# config.mock_with :rr
|
37
|
+
config.mock_with :rspec
|
38
|
+
|
39
|
+
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
40
|
+
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
41
|
+
|
42
|
+
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
43
|
+
# examples within a transaction, remove the following line or assign false
|
44
|
+
# instead of true.
|
45
|
+
config.use_transactional_fixtures = true
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: solidus_fulfillment
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rémy Coutable
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-04-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: solidus_core
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: active_fulfillment
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Solidus extension to do fulfillment processing via various services when
|
42
|
+
a shipment becomes ready
|
43
|
+
email: remy@rymai.me
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- ".rspec"
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- Versionfile
|
55
|
+
- app/models/spree/fulfillment.rb
|
56
|
+
- app/models/spree/shipment_decorator.rb
|
57
|
+
- lib/generators/solidus/fulfillment/install/install_generator.rb
|
58
|
+
- lib/solidus/fulfillment/amazon_fulfillment.rb
|
59
|
+
- lib/solidus/fulfillment/engine.rb
|
60
|
+
- lib/solidus_fulfillment.rb
|
61
|
+
- lib/tasks/solidus_fulfillment.rake
|
62
|
+
- script/rails
|
63
|
+
- solidus_fulfillment.gemspec
|
64
|
+
- spec/spec_helper.rb
|
65
|
+
homepage: https://rubygems.org/gems/solidus_fulfillment
|
66
|
+
licenses: []
|
67
|
+
metadata: {}
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 2.2.7
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements:
|
83
|
+
- none
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 2.5.2
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: Solidus extension to do fulfillment processing via various services when
|
89
|
+
a shipment becomes ready
|
90
|
+
test_files:
|
91
|
+
- spec/spec_helper.rb
|