spree-returnly 0.13.0

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