solidus-returnly 0.6.1 → 0.6.2
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 +4 -4
- data/app/models/reimbursement_shipping.rb +61 -0
- data/app/services/refund_methods.rb +21 -0
- data/app/services/refund_payments.rb +33 -0
- data/lib/returnly.rb +1 -0
- data/lib/returnly/refund/amount_calculator.rb +47 -4
- data/lib/returnly/refund_calculator.rb +5 -1
- data/lib/returnly/refund_presenter.rb +7 -16
- data/lib/returnly/refunder.rb +19 -9
- data/lib/returnly/services/create_reimbursement.rb +74 -0
- data/lib/returnly/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cdcc6a311bd8b506654e3f98002a2e5d50c5149f
|
4
|
+
data.tar.gz: 639068f9abf9fdb9aa33b34f7315bd898a217234
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eee6692d5dfdf2c0c23ea2dafa60c42b4788dae0f6828962f1fc41f1b239cda93da934f448c0ecc290f12aa397015a631e254432e4bb8b98768e4a44bdae8593
|
7
|
+
data.tar.gz: 1cd8a79ad38584d16619574af858a3759a0c3f4516ff2c04b1e5dce8e7eaa7e476d17f86e5ae62d5fafc6c4b8d8e3a429af97b506f8e5c99103bc5724cf6dc7c
|
@@ -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,21 @@
|
|
1
|
+
module RefundMethods
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
included do
|
4
|
+
class_attribute :eligible_refund_methods
|
5
|
+
self.eligible_refund_methods = whitelisted_refund_methods || defined_refund_methods
|
6
|
+
end
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def whitelisted_refund_methods
|
10
|
+
# Solidus 2.3.0 deprecates "Spree::Gateway::Bogus" and recommends using "Spree::PaymentMethod::BogusCreditCard" instead.
|
11
|
+
%w[Spree::Gateway::Bogus Spree::PaymentMethod::SimpleBogusCreditCard Spree::PaymentMethod::BogusCreditCard Spree::PaymentMethod::Check Spree::PaymentMethod::StoreCredit].inject([]) do |methods, payment_method|
|
12
|
+
methods << payment_method.constantize if Object.const_defined?(payment_method)
|
13
|
+
methods
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def defined_refund_methods
|
18
|
+
Spree::PaymentMethod.order('id DESC').pluck(:type).map(&:constantize)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class RefundPayments
|
2
|
+
attr_accessor :order
|
3
|
+
|
4
|
+
include RefundMethods
|
5
|
+
|
6
|
+
def initialize(order)
|
7
|
+
@order = order
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform!(payment_amount)
|
11
|
+
sorted_payments = sorted_eligible_refund_payments(order.payments.completed)
|
12
|
+
sorted_payments.each_with_object([]) do |payment, payments|
|
13
|
+
break payments if payment_amount <= 0
|
14
|
+
next payments unless payment.can_credit?
|
15
|
+
|
16
|
+
allowed_amount = [payment_amount, payment.credit_allowed].min
|
17
|
+
payment_amount -= allowed_amount
|
18
|
+
|
19
|
+
payments << {
|
20
|
+
code: payment.source_type.constantize.model_name.human,
|
21
|
+
amount: allowed_amount.to_f,
|
22
|
+
original_transaction_id: ''
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def sorted_eligible_refund_payments(payments)
|
30
|
+
payments = payments.select { |p| eligible_refund_methods.include? p.payment_method.class }
|
31
|
+
payments.sort_by { |p| eligible_refund_methods.index(p.payment_method.class) }
|
32
|
+
end
|
33
|
+
end
|
data/lib/returnly.rb
CHANGED
@@ -8,13 +8,56 @@ module Returnly
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def return_item_refund_amount(return_item)
|
11
|
-
|
11
|
+
(item_price(return_item) * item_price_percentage / 100).round(2, :down)
|
12
12
|
end
|
13
13
|
|
14
14
|
protected
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
|
16
|
+
def total_single_items_price
|
17
|
+
refund.line_items.inject(0.0.to_d) do |amount, line_item_hash|
|
18
|
+
line_item = Spree::LineItem.find(line_item_hash['order_line_item_id'])
|
19
|
+
amount += (line_item_price(line_item) * line_item_hash['units'].to_d).round(2, :down)
|
20
|
+
amount
|
21
|
+
end.round(2, :down)
|
22
|
+
end
|
23
|
+
|
24
|
+
def line_item_price(line_item)
|
25
|
+
(line_item.price + line_item_tax_price(line_item) + line_item_discount_price(line_item)).round(2, :down)
|
26
|
+
end
|
27
|
+
|
28
|
+
def item_price_percentage
|
29
|
+
[(available_amount * 100 / total_single_items_price).round(2, :down), 100.0.to_d].min
|
30
|
+
end
|
31
|
+
|
32
|
+
def item_price(return_item)
|
33
|
+
line_item_price(return_item.inventory_unit.line_item)
|
34
|
+
end
|
35
|
+
|
36
|
+
def line_item_tax_price(line_item)
|
37
|
+
(line_item.adjustments.tax.sum(&:amount).to_d.round(2, :down) / line_item.quantity).round(2, :down)
|
38
|
+
end
|
39
|
+
|
40
|
+
def line_item_discount_price(line_item)
|
41
|
+
discounter_calculator.calculate(
|
42
|
+
[
|
43
|
+
{
|
44
|
+
order_line_item_id: line_item.id,
|
45
|
+
units: 1
|
46
|
+
}
|
47
|
+
]
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def line_items_price
|
52
|
+
refund.order.line_items.sum(&:price).round(2, :down)
|
53
|
+
end
|
54
|
+
|
55
|
+
def available_amount
|
56
|
+
refund.product_available_amount.round(2, :down)
|
57
|
+
end
|
58
|
+
|
59
|
+
def discounter_calculator
|
60
|
+
@discounter_calculator ||= Returnly::DiscountCalculator.new(refund.order)
|
18
61
|
end
|
19
62
|
end
|
20
63
|
end
|
@@ -29,7 +29,11 @@ module Returnly
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def shipping_tax
|
32
|
-
order.all_adjustments.where(source_type: 'Spree::TaxRate', adjustable_type: 'Spree::Shipment').sum(&:amount)
|
32
|
+
order.all_adjustments.where(source_type: 'Spree::TaxRate', adjustable_type: 'Spree::Shipment').sum(&:amount).to_d.round(2, :down)
|
33
|
+
end
|
34
|
+
|
35
|
+
def line_item_tax(line_item)
|
36
|
+
(line_item.adjustments.tax.sum(&:amount).to_d / line_item.quantity).round(2, :down)
|
33
37
|
end
|
34
38
|
|
35
39
|
def line_items_return_items
|
@@ -2,25 +2,25 @@ module Returnly
|
|
2
2
|
class RefundPresenter
|
3
3
|
|
4
4
|
def self.present_estimate(calculator)
|
5
|
-
total = Money.new 0
|
6
5
|
sub_total = Money.new 0
|
6
|
+
tax = Money.new 0
|
7
7
|
|
8
8
|
calculator.line_items_return_items.values.flatten.each do |return_item|
|
9
|
-
total += Money.from_amount return_item.total
|
10
9
|
sub_total += Money.from_amount return_item.pre_tax_amount
|
10
|
+
tax += Money.from_amount calculator.line_item_tax(return_item.inventory_unit.line_item)
|
11
11
|
end
|
12
12
|
|
13
13
|
discount = Money.from_amount(calculator.discount)
|
14
|
-
total_refund_amount =
|
14
|
+
total_refund_amount = sub_total + tax - (discount * -1) # discount is negative, so we multiply it by -1 to make it positive for subtraction
|
15
15
|
shipping_tax = calculator.shipping_tax
|
16
16
|
{
|
17
17
|
product_amount: sub_total.to_f,
|
18
|
-
tax_amount:
|
18
|
+
tax_amount: tax.to_f,
|
19
19
|
discount_amount: discount.to_f,
|
20
20
|
total_refund_amount: total_refund_amount.to_f,
|
21
21
|
refundable_shipping_amount: calculator.order.shipment_total.to_f,
|
22
22
|
refundable_shipping_tax_amount: shipping_tax.to_f,
|
23
|
-
max_refundable_amount: (calculator.order.total - calculator.order.refunds.sum(&:amount)).to_f,
|
23
|
+
max_refundable_amount: (calculator.order.total - calculator.order.refunds.reload.sum(&:amount)).to_f,
|
24
24
|
refunds_by_payment_method: refunds_by_payment(calculator.order, total_refund_amount.to_d + calculator.order.shipment_total + shipping_tax)
|
25
25
|
}
|
26
26
|
end
|
@@ -68,17 +68,8 @@ module Returnly
|
|
68
68
|
}
|
69
69
|
end
|
70
70
|
|
71
|
-
private_class_method def self.refunds_by_payment(order,
|
72
|
-
|
73
|
-
payment_amount = amount / completed_payments.count
|
74
|
-
|
75
|
-
completed_payments.each_with_object([]) do |payment, payments|
|
76
|
-
payments << {
|
77
|
-
code: payment.source_type.constantize.model_name.human,
|
78
|
-
amount: payment_amount.to_f,
|
79
|
-
original_transaction_id: ''
|
80
|
-
}
|
81
|
-
end
|
71
|
+
private_class_method def self.refunds_by_payment(order, payment_amount)
|
72
|
+
::RefundPayments.new(order).perform!(payment_amount)
|
82
73
|
end
|
83
74
|
end
|
84
75
|
end
|
data/lib/returnly/refunder.rb
CHANGED
@@ -23,8 +23,9 @@ module Returnly
|
|
23
23
|
|
24
24
|
def process_return_items
|
25
25
|
each_return_item do |return_item|
|
26
|
-
return_item.amount
|
27
|
-
return_item.
|
26
|
+
return_item.amount = return_item_amount_calculator.return_item_refund_amount return_item
|
27
|
+
return_item.additional_tax_total = 0
|
28
|
+
return_item.resellable = return_item_restock_policy.should_return_item? return_item
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
@@ -39,19 +40,28 @@ module Returnly
|
|
39
40
|
end
|
40
41
|
|
41
42
|
def refund_available_amount
|
42
|
-
@available_amount ||= Money.from_amount(
|
43
|
-
end
|
44
|
-
|
45
|
-
def refund_amount_per_item
|
46
|
-
refund_available_amount / line_items.size
|
43
|
+
@available_amount ||= Money.from_amount(product_available_amount)
|
47
44
|
end
|
48
45
|
|
49
46
|
def reimbursement
|
50
47
|
@_reimbursement ||= Spree::Reimbursement.build_from_customer_return(customer_return)
|
51
48
|
end
|
52
49
|
|
50
|
+
def product_available_amount
|
51
|
+
total = order.total - (order.shipment_total + refund_calculator.shipping_tax + refunds)
|
52
|
+
[total, product_refund_amount.to_d].min
|
53
|
+
end
|
54
|
+
|
53
55
|
private
|
54
56
|
|
57
|
+
def add_shipping_amount!
|
58
|
+
::ReimbursementShipping.new(reimbursement).update!(shipping_refund_amount.to_d)
|
59
|
+
end
|
60
|
+
|
61
|
+
def refunds
|
62
|
+
order.refunds.sum(&:amount).to_d.round(2, :down)
|
63
|
+
end
|
64
|
+
|
55
65
|
def configure
|
56
66
|
self.return_item_amount_calculator = return_item_amount_calculator_class.new(self)
|
57
67
|
self.return_item_restock_policy = return_item_restock_policy_class.new(self)
|
@@ -64,8 +74,8 @@ module Returnly
|
|
64
74
|
end
|
65
75
|
|
66
76
|
def perform_reimbursement
|
67
|
-
reimbursement.
|
68
|
-
|
77
|
+
Returnly::Services::CreateReimbursement.new(reimbursement).perform!
|
78
|
+
add_shipping_amount!
|
69
79
|
end
|
70
80
|
|
71
81
|
def reimburse_without_items!
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Returnly
|
2
|
+
module Services
|
3
|
+
class CreateReimbursement
|
4
|
+
attr_accessor :reimbursement
|
5
|
+
|
6
|
+
delegate :return_items, to: :reimbursement
|
7
|
+
|
8
|
+
def initialize(reimbursement)
|
9
|
+
@reimbursement = reimbursement
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform!
|
13
|
+
reimbursement.save!
|
14
|
+
reimbursement.update!(total: calculated_total)
|
15
|
+
performer.perform(reimbursement)
|
16
|
+
|
17
|
+
if unpaid_amount_within_tolerance?
|
18
|
+
reimbursement.reimbursed!
|
19
|
+
Spree::Reimbursement.reimbursement_success_hooks.each { |h| h.call reimbursement }
|
20
|
+
send_reimbursement_email
|
21
|
+
else
|
22
|
+
reimbursement.errored!
|
23
|
+
Spree::Reimbursement.reimbursement_failure_hooks.each { |h| h.call reimbursement }
|
24
|
+
raise incomplete_error_class, Spree.t('validation.unpaid_amount_not_zero', amount: unpaid_amount)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def incomplete_error_class
|
31
|
+
Spree::Reimbursement::IncompleteReimbursementError
|
32
|
+
end
|
33
|
+
|
34
|
+
def reimbursement_models
|
35
|
+
Spree::Reimbursement.reimbursement_models
|
36
|
+
end
|
37
|
+
|
38
|
+
def paid_amount
|
39
|
+
reimbursement_models.sum do |model|
|
40
|
+
model.total_amount_reimbursed_for(reimbursement)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def unpaid_amount
|
45
|
+
reimbursement.total - paid_amount
|
46
|
+
end
|
47
|
+
|
48
|
+
def calculated_total
|
49
|
+
return_items.to_a.sum(&:total).to_d.round(2, :down)
|
50
|
+
end
|
51
|
+
|
52
|
+
def performer
|
53
|
+
Spree::Reimbursement.reimbursement_performer
|
54
|
+
end
|
55
|
+
|
56
|
+
def send_reimbursement_email
|
57
|
+
Spree::ReimbursementMailer.reimbursement_email(reimbursement.id).deliver_later
|
58
|
+
end
|
59
|
+
|
60
|
+
def unpaid_amount_within_tolerance?
|
61
|
+
reimbursement_count = reimbursement_models.count do |model|
|
62
|
+
model.total_amount_reimbursed_for(reimbursement) > 0
|
63
|
+
end
|
64
|
+
|
65
|
+
leniency = if reimbursement_count > 0
|
66
|
+
(reimbursement_count - 1) * 0.01.to_d
|
67
|
+
else
|
68
|
+
0
|
69
|
+
end
|
70
|
+
unpaid_amount.abs.between?(0, leniency)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/returnly/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solidus-returnly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Returnly Technologies, Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-09-
|
11
|
+
date: 2017-09-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: solidus
|
@@ -330,8 +330,11 @@ files:
|
|
330
330
|
- app/controllers/spree/api/returnly/api_controller.rb
|
331
331
|
- app/controllers/spree/api/returnly/refunds_controller.rb
|
332
332
|
- app/controllers/spree/api/returnly/version_controller.rb
|
333
|
+
- app/models/reimbursement_shipping.rb
|
333
334
|
- app/models/reimbursement_type/original_payment_no_items.rb
|
334
335
|
- app/models/spree/return_item_decorator.rb
|
336
|
+
- app/services/refund_methods.rb
|
337
|
+
- app/services/refund_payments.rb
|
335
338
|
- config/locales/en.yml
|
336
339
|
- config/routes.rb
|
337
340
|
- lib/returnly.rb
|
@@ -346,6 +349,7 @@ files:
|
|
346
349
|
- lib/returnly/refund_presenter.rb
|
347
350
|
- lib/returnly/refunder.rb
|
348
351
|
- lib/returnly/refunds_configuration.rb
|
352
|
+
- lib/returnly/services/create_reimbursement.rb
|
349
353
|
- lib/returnly/version.rb
|
350
354
|
- lib/solidus-returnly.rb
|
351
355
|
- lib/spree-returnly.rb
|