spree-returnly 0.13.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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +230 -0
  4. data/Rakefile +46 -0
  5. data/app/controllers/spree/api/returnly/api_controller.rb +14 -0
  6. data/app/controllers/spree/api/returnly/refunds_controller.rb +43 -0
  7. data/app/controllers/spree/api/returnly/version_controller.rb +14 -0
  8. data/app/models/reimbursement_shipping.rb +61 -0
  9. data/app/models/reimbursement_type/original_payment_no_items.rb +17 -0
  10. data/app/models/spree/return_item_decorator.rb +14 -0
  11. data/app/services/refund_methods.rb +20 -0
  12. data/app/services/refund_payments.rb +59 -0
  13. data/config/locales/en.yml +5 -0
  14. data/config/routes.rb +8 -0
  15. data/lib/returnly.rb +17 -0
  16. data/lib/returnly/builders/customer_return.rb +28 -0
  17. data/lib/returnly/builders/return_item.rb +42 -0
  18. data/lib/returnly/discount_calculator.rb +51 -0
  19. data/lib/returnly/discounts/line_item.rb +26 -0
  20. data/lib/returnly/discounts/order.rb +33 -0
  21. data/lib/returnly/engine.rb +69 -0
  22. data/lib/returnly/factories.rb +6 -0
  23. data/lib/returnly/gift_card.rb +21 -0
  24. data/lib/returnly/refund/amount_calculator.rb +64 -0
  25. data/lib/returnly/refund/return_item_restock_policy.rb +15 -0
  26. data/lib/returnly/refund_calculator.rb +62 -0
  27. data/lib/returnly/refund_presenter.rb +115 -0
  28. data/lib/returnly/refunder.rb +110 -0
  29. data/lib/returnly/refunds_configuration.rb +36 -0
  30. data/lib/returnly/services/create_reimbursement.rb +74 -0
  31. data/lib/returnly/services/mark_items_as_returned.rb +25 -0
  32. data/lib/returnly/version.rb +7 -0
  33. data/lib/solidus-returnly.rb +12 -0
  34. data/lib/spree-returnly.rb +12 -0
  35. metadata +400 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: abacb9ca4551621d1cecf6e0c51003b692606e7b58f6d8a235f586aec75f216a
4
+ data.tar.gz: 6be91cd2cd61ef37257e433b5ebdf81aa7adc5e8e761ff40b2a602cab9722f10
5
+ SHA512:
6
+ metadata.gz: 3491d355992e2084e9a47acf969a2106251528da9e8398dc06e9df17f43e24f75381d8a29b626062b657ceb2a7f138020a083a41ff60b38ba723a223722f5eed
7
+ data.tar.gz: 67426fe2170d1bbd41b13ecbdbd59020865b5cb6fb42b2c096b83a36a9d7d98aad06df15e4ccf8f880a5bb291ee6b0f4009292849d5457b8965c6e22e4b25bad
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2017 Returnly Technologies, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,230 @@
1
+ Solidus Returnly
2
+ ===============
3
+
4
+ This gem adds the required APIs to allow Returnly to obtain an estimate, process a return, and apply a reimbursement
5
+
6
+
7
+ Spree/Solidus
8
+ -----
9
+ This gem can be installed on spree and solidus, to release a new version to both registries you need to run
10
+ `bash
11
+ $ rake both_releases
12
+ `
13
+ this will generate two packages, `solidus-returnly` and `spree-returnly`
14
+
15
+ Current Compat Versions:
16
+ - Solidus : `~> 2.4.0`
17
+ - Spree : `~> 3.1.0`
18
+
19
+ Installation
20
+ ------------
21
+
22
+ Add solidus_returnly to your Gemfile:
23
+
24
+ ```ruby
25
+ gem 'solidus-returnly'
26
+ ```
27
+
28
+ Bundle your dependencies and run the installation generator:
29
+
30
+ ```shell
31
+ bundle
32
+ bundle exec rails g solidus_returnly:install
33
+ ```
34
+
35
+ Testing
36
+ -------
37
+
38
+ First bundle your dependencies, then run `rake`. `rake` will default to building the dummy app if it does not exist, then it will run specs, and [Rubocop](https://github.com/bbatsov/rubocop) static code analysis. The dummy app can be regenerated by using `rake test_app`.
39
+
40
+ ```shell
41
+ bundle
42
+ bundle exec rake
43
+ ```
44
+
45
+ When testing your applications integration with this extension you may use it's factories.
46
+ Simply add this require statement to your spec_helper:
47
+
48
+ ```ruby
49
+ require 'solidus/returnly/factories'
50
+ ```
51
+
52
+ Solidus/Spree Sandbox
53
+ ---------------------
54
+
55
+ You can create a sandbox spree installationm
56
+ ```shell
57
+ git clone git@github.com:solidusio/solidus.git
58
+ cd solidus
59
+ git co v2.0
60
+ bundle install
61
+ rake sandbox
62
+ cd sandbox
63
+ rake spree_auth:admin:create
64
+ ```
65
+
66
+ default admin credentials:
67
+ ```
68
+ Email [admin@example.com]
69
+ Password [test123]
70
+ ```
71
+
72
+ this will create a pristine solidus installation you can add the `solidus_returnly` gem and test the endpoints live
73
+
74
+ API
75
+ ---
76
+
77
+ To get an estimate of the return order and taxes
78
+
79
+ `POST /api/returnly/orders/:order_number/refund_estimate`
80
+
81
+ Payload:
82
+ ```json
83
+ {
84
+ "items": [
85
+ {
86
+ "order_line_item_id": "{spree order line item id}",
87
+ "units": 1
88
+ }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ Response:
94
+
95
+ ```json
96
+ {
97
+ "product_amount": "{money}",
98
+ "tax_amount": "{money}",
99
+ "discount_amount": "{money}",
100
+ "total_refund_amount": "{money}",
101
+ "refundable_shipping_amount": "{money}",
102
+ "refundable_shipping_tax_amount": "{money}",
103
+ "max_refundable_amount": "{money}",
104
+ "transactions": [
105
+ {
106
+ "id": "{refund tx id}",
107
+ "amount": "{money}",
108
+ "status": "PENDING",
109
+ "type": "REFUND",
110
+ "gateway": "{gateway}",
111
+ "is_online": true,
112
+ "is_test": true,
113
+ "payment_details": {
114
+ "avs_result_code": "{code}",
115
+ "cvv_result_code": "{code}",
116
+ "credit_card_number": "{number}",
117
+ "credit_card_company": "visa",
118
+ "gift_card_id": "{voucher_id}",
119
+ "gift_card_code": "{voucher_code}"
120
+ },
121
+ "created_at": "{date}"
122
+ }
123
+ ]
124
+ }
125
+ ```
126
+
127
+ To process the return and apply the reimbursement**immediately**
128
+ you can pass the `restock` parameter on false to prevent the api
129
+ to immediately change the stock in the default warehouse
130
+
131
+ `POST /api/returnly/orders/:order_number/refund`
132
+
133
+ Payload:
134
+
135
+ ```json
136
+ {
137
+ "items": [
138
+ {
139
+ "order_line_item_id": "{x_order_line_item_id1}",
140
+ "units": 2,
141
+ "restock": true
142
+ }
143
+ ],
144
+ "notify_shopper": true,
145
+ "product_refund_amount": "{money}",
146
+ "shipping_refund_amount": "{money}",
147
+ "refund_note": "comment text"
148
+ }
149
+ ```
150
+
151
+ Response:
152
+
153
+ ```json
154
+ {
155
+ "refund_id": "{x_refund_id}",
156
+ "user_id": "{optional merchant user ID}",
157
+ "line_items": [
158
+ {
159
+ "refund_line_item_id": "{x_refund_line_item_id1}",
160
+ "order_line_item_id": "{x_order_line_item_id1}",
161
+ "units": 2
162
+ },
163
+ ],
164
+ "transactions": [
165
+ {
166
+ "id": "{refund tx id}",
167
+ "amount": "{money}",
168
+ "status": "PENDING",
169
+ "type": "REFUND",
170
+ "gateway": "{gateway}",
171
+ "is_online": true,
172
+ "is_test": true,
173
+ "payment_details": {
174
+ "avs_result_code": "{code}",
175
+ "cvv_result_code": "{code}",
176
+ "credit_card_number": "{number}",
177
+ "credit_card_company": "visa",
178
+ "gift_card_id": "{voucher_id}",
179
+ "gift_card_code": "{voucher_code}"
180
+ }
181
+ }
182
+ ],
183
+ "created_at": "{date}",
184
+ "updated_at": "{date}",
185
+ "refund_note": "comment text",
186
+ }
187
+ ```
188
+
189
+
190
+ Overriding Refund Logic
191
+ --------------
192
+
193
+ Is possible to override the default behavior of the Refund Gem by extending the following classes
194
+
195
+ `lib/returnly/refund/return_item_restock_policy.rb`
196
+
197
+ ```ruby
198
+ class CustomReturnItemRestockPolicy < ReturnItemRestockPolicy
199
+ def should_return_item?(return_item)
200
+ return_item.cost > 1
201
+ end
202
+ end
203
+ ```
204
+
205
+ `lib/returnly/refund/amount_calculator.rb`
206
+
207
+ ```ruby
208
+ class CustomAmountCalculator < AmountCalculator
209
+ def return_item_refund_amount(return_item)
210
+ default_amount(return_item) - 5.0
211
+ end
212
+ end
213
+ ```
214
+
215
+ to set your custom classes to the returns process just
216
+ add an initializer file: `cofnig/initializers/returnly.rb`
217
+
218
+ ```ruby
219
+ Returnly.configure do |config|
220
+ config.return_item_amount_calculator = CustomAmountCalculator
221
+ config.return_item_restock_policy = CustomReturnItemRestockPolicy
222
+ config.refunder = CustomRefunder
223
+ config.refund_calculator = CustomRefunderCalculator
224
+ config.reimbursement_type_no_items = CustomOriginalPaymentNoItems
225
+ config.refund_presenter = CustomRefundPresenter
226
+ config.return_item_builder = CustomReturnItemBuilder
227
+ end
228
+ ```
229
+
230
+ Copyright (c) 2017 Returnly Technologies, Inc
@@ -0,0 +1,46 @@
1
+ $:.push File.expand_path('../lib', __FILE__)
2
+ require 'bundler'
3
+
4
+ def load_library_env
5
+ lib_name = ENV['SPREE_BUILD'] ? 'spree-returnly' : 'solidus-returnly'
6
+
7
+ puts "Loading #{lib_name}"
8
+
9
+ Bundler::GemHelper.install_tasks name: lib_name
10
+ end
11
+
12
+
13
+ load_library_env
14
+
15
+ begin
16
+ require 'spree/testing_support/extension_rake'
17
+ require 'rspec/core/rake_task'
18
+
19
+ RSpec::Core::RakeTask.new(:spec)
20
+
21
+ task default: %i(first_run spec)
22
+ rescue LoadError
23
+ # no rspec available
24
+ end
25
+
26
+ task :first_run do
27
+ if Dir['spec/dummy'].empty?
28
+ Rake::Task[:test_app].invoke
29
+ Dir.chdir('../../')
30
+ end
31
+ end
32
+
33
+ task :both_releases do
34
+ %w(spree-returnly solidus-returnly).each do |lib_name|
35
+ Bundler::GemHelper.install_tasks name: lib_name
36
+ Rake::Task[:release].invoke
37
+ end
38
+
39
+ load_library_env
40
+ end
41
+
42
+ desc 'Generates a dummy app for testing'
43
+ task :test_app do
44
+ ENV['LIB_NAME'] = 'solidus-returnly'
45
+ Rake::Task['extension:test_app'].invoke
46
+ end
@@ -0,0 +1,14 @@
1
+ module Spree
2
+ module Api
3
+ module Returnly
4
+ class ApiController < Spree::Api::BaseController
5
+ before_action :set_headers
6
+
7
+ def set_headers
8
+ response.headers['X-Returnly-Extension-Version'] = ::Returnly::VERSION
9
+ response.headers['X-Returnly-Extension-Platform-Qualifier'] = ::Returnly.platform_version
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ module Spree
2
+ module Api
3
+ module Returnly
4
+ class RefundsController < ApiController
5
+ include ::Returnly::RefundsConfiguration
6
+
7
+ def estimate
8
+ authorize! :create, Order
9
+ render json: refund_calculator_class.process(order, line_items_params)
10
+ end
11
+
12
+ def create
13
+ authorize! :create, Order
14
+
15
+ refunds = refunder_class.new order: order,
16
+ line_items: line_items_params,
17
+ product_refund_amount: product_refund_amount_param,
18
+ shipping_refund_amount: shipping_refund_amount_param
19
+ render json: refunds.proceed!
20
+ end
21
+
22
+ private
23
+
24
+ def line_items_params
25
+ return [] if params[:items].blank?
26
+ params.require(:items)
27
+ end
28
+
29
+ def product_refund_amount_param
30
+ params.require(:product_refund_amount)
31
+ end
32
+
33
+ def shipping_refund_amount_param
34
+ params.require(:shipping_refund_amount)
35
+ end
36
+
37
+ def order
38
+ @order ||= Spree::Order.find_by!(number: params[:order_id])
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ module Spree
2
+ module Api
3
+ module Returnly
4
+ class VersionController < ApiController
5
+ def index
6
+ render json: {
7
+ extension_version: ::Returnly::VERSION,
8
+ platform_version: ::Returnly.platform_version
9
+ }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ class ReimbursementShipping
2
+ attr_accessor :reimbursement
3
+ attr_reader :order
4
+
5
+ include Spree::ReimbursementType::ReimbursementHelpers
6
+
7
+ def initialize(reimbursement)
8
+ @order = reimbursement.order
9
+ @reimbursement = reimbursement
10
+ end
11
+
12
+ def update!(shipping_amount)
13
+ reimbursement.update!(total: calculated_amount(shipping_amount))
14
+ create_refund_payment!(shipping_amount)
15
+ end
16
+
17
+ private
18
+
19
+ def calculate_shipping_amount(shipping_amount)
20
+ [shipping_amount, available_shipment_amount].min
21
+ end
22
+
23
+ def calculated_amount(shipping_amount)
24
+ calculate_shipping_amount(shipping_amount) + reimbursement.total
25
+ end
26
+
27
+ def available_shipment_amount
28
+ order.shipment_total + order_shipping_tax - refunded_shipping
29
+ end
30
+
31
+ def refunded_shipping
32
+ order.refunds.where(refund_reason_id: refund_reason.id).sum(&:amount)
33
+ end
34
+
35
+ def create_refund_payment!(shipping_amount)
36
+ create_refunds(reimbursement, payments, calculate_shipping_amount(shipping_amount), false)
37
+ end
38
+
39
+ def order_shipping_tax
40
+ order.all_adjustments.where(source_type: 'Spree::TaxRate', adjustable_type: 'Spree::Shipment').sum(&:amount)
41
+ end
42
+
43
+ def payments
44
+ reimbursement.order.payments.completed
45
+ end
46
+
47
+ def create_refund(reimbursement, payment, amount, simulate)
48
+ refund = reimbursement.refunds.build({
49
+ payment: payment,
50
+ amount: amount,
51
+ reason: refund_reason
52
+ })
53
+
54
+ simulate ? refund.readonly! : refund.save!
55
+ refund
56
+ end
57
+
58
+ def refund_reason
59
+ @refund_reason ||= Spree::RefundReason.find_or_create_by!(name: 'Returnly Shipping Refund')
60
+ end
61
+ end
@@ -0,0 +1,17 @@
1
+ module ReimbursementType
2
+ class OriginalPaymentNoItems < Spree::ReimbursementType
3
+ extend Spree::ReimbursementType::ReimbursementHelpers
4
+
5
+ class << self
6
+ def reimburse(reimbursement, _return_items, simulate)
7
+ unpaid_amount = reimbursement.total.round(2, :down)
8
+ payments = reimbursement.order.payments.completed
9
+
10
+ refund_list, _unpaid_amount = create_refunds(reimbursement, payments, unpaid_amount, simulate)
11
+ reimbursement.update(total: refund_list.sum(&:amount))
12
+ refund_list
13
+ end
14
+ end
15
+ end
16
+
17
+ end