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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +230 -0
- data/Rakefile +46 -0
- data/app/controllers/spree/api/returnly/api_controller.rb +14 -0
- data/app/controllers/spree/api/returnly/refunds_controller.rb +43 -0
- data/app/controllers/spree/api/returnly/version_controller.rb +14 -0
- data/app/models/reimbursement_shipping.rb +61 -0
- data/app/models/reimbursement_type/original_payment_no_items.rb +17 -0
- data/app/models/spree/return_item_decorator.rb +14 -0
- data/app/services/refund_methods.rb +20 -0
- data/app/services/refund_payments.rb +59 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +8 -0
- data/lib/returnly.rb +17 -0
- data/lib/returnly/builders/customer_return.rb +28 -0
- data/lib/returnly/builders/return_item.rb +42 -0
- data/lib/returnly/discount_calculator.rb +51 -0
- data/lib/returnly/discounts/line_item.rb +26 -0
- data/lib/returnly/discounts/order.rb +33 -0
- data/lib/returnly/engine.rb +69 -0
- data/lib/returnly/factories.rb +6 -0
- data/lib/returnly/gift_card.rb +21 -0
- data/lib/returnly/refund/amount_calculator.rb +64 -0
- data/lib/returnly/refund/return_item_restock_policy.rb +15 -0
- data/lib/returnly/refund_calculator.rb +62 -0
- data/lib/returnly/refund_presenter.rb +115 -0
- data/lib/returnly/refunder.rb +110 -0
- data/lib/returnly/refunds_configuration.rb +36 -0
- data/lib/returnly/services/create_reimbursement.rb +74 -0
- data/lib/returnly/services/mark_items_as_returned.rb +25 -0
- data/lib/returnly/version.rb +7 -0
- data/lib/solidus-returnly.rb +12 -0
- data/lib/spree-returnly.rb +12 -0
- metadata +400 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
Spree::ReturnItem.class_eval do
|
2
|
+
unless ::Returnly.is_solidus?
|
3
|
+
attr_accessor :amount
|
4
|
+
|
5
|
+
def amount
|
6
|
+
self.total_excluding_vat + self.included_tax_total
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
def amount=(amount)
|
11
|
+
self.total_excluding_vat = amount - self.included_tax_total
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RefundMethods
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
included do
|
4
|
+
class_attribute :eligible_refund_methods
|
5
|
+
self.eligible_refund_methods = whitelisted_refund_methods.empty? ? defined_refund_methods : whitelisted_refund_methods
|
6
|
+
end
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def whitelisted_refund_methods
|
10
|
+
# Override this methods to whitelist payment methods
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
|
14
|
+
def defined_refund_methods
|
15
|
+
payment_methods = Spree::PaymentMethod.pluck(:type)
|
16
|
+
sorted_payment_methods = payment_methods.reject { |payment_method| payment_method =~ /StoreCredit/ }
|
17
|
+
(sorted_payment_methods | payment_methods).map(&:constantize)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
class RefundPayments
|
2
|
+
attr_accessor :order
|
3
|
+
|
4
|
+
include RefundMethods
|
5
|
+
|
6
|
+
def initialize(order, gift_card)
|
7
|
+
@order = order
|
8
|
+
@gift_card = gift_card
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform!(payment_amount)
|
12
|
+
sorted_payments = sorted_eligible_refund_payments(order.payments.completed)
|
13
|
+
sorted_payments.each_with_object([]) do |payment, payments|
|
14
|
+
break payments if payment_amount <= 0
|
15
|
+
next payments unless payment.can_credit?
|
16
|
+
|
17
|
+
allowed_amount = [payment_amount, payment.credit_allowed].min
|
18
|
+
payment_amount -= allowed_amount
|
19
|
+
|
20
|
+
payments << {
|
21
|
+
id: payment.id.to_s,
|
22
|
+
amount: allowed_amount.to_s,
|
23
|
+
status: 'PENDING',
|
24
|
+
type: 'REFUND',
|
25
|
+
gateway: payment.payment_method.type.demodulize,
|
26
|
+
is_online: true,
|
27
|
+
is_test: !Rails.env.match?(/production/),
|
28
|
+
payment_details: transaction_payment_details(payment),
|
29
|
+
created_at: Time.zone.now.iso8601(3)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def sorted_eligible_refund_payments(payments)
|
37
|
+
payments = payments.select { |p| eligible_refund_methods.include? p.payment_method.class }
|
38
|
+
payments.sort_by { |p| eligible_refund_methods.index(p.payment_method.class) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def transaction_payment_details(payment)
|
42
|
+
details = {}
|
43
|
+
|
44
|
+
details[:avs_result_code] = payment.avs_response if payment.avs_response.present?
|
45
|
+
details[:cvv_result_code] = payment.cvv_response_code if payment.cvv_response_code.present?
|
46
|
+
|
47
|
+
source = payment.source
|
48
|
+
|
49
|
+
if source.respond_to?(:number) || source.respond_to?(:last_digits)
|
50
|
+
details[:credit_card_number] = source.number || source.last_digits if source.number.present? || source.last_digits.present?
|
51
|
+
details[:credit_card_company] = source.brand if source.brand.present?
|
52
|
+
end
|
53
|
+
|
54
|
+
details[:gift_card_id] = @gift_card.id if @gift_card.id.present?
|
55
|
+
details[:gift_card_code] = @gift_card.code if @gift_card.code.present?
|
56
|
+
|
57
|
+
details
|
58
|
+
end
|
59
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
Spree::Core::Engine.routes.draw do
|
2
|
+
namespace :api, defaults: { format: 'json' } do
|
3
|
+
post '/returnly/orders/:order_id/refund_estimate', to: 'returnly/refunds#estimate'
|
4
|
+
post '/returnly/orders/:order_id/refund', to: 'returnly/refunds#create'
|
5
|
+
|
6
|
+
get '/returnly/version', to: 'returnly/version#index'
|
7
|
+
end
|
8
|
+
end
|
data/lib/returnly.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'returnly/engine'
|
2
|
+
require 'returnly/refunds_configuration'
|
3
|
+
require 'returnly/refunder'
|
4
|
+
require 'returnly/discounts/order'
|
5
|
+
require 'returnly/discounts/line_item'
|
6
|
+
require 'returnly/discount_calculator'
|
7
|
+
require 'returnly/builders/customer_return'
|
8
|
+
require 'returnly/builders/return_item'
|
9
|
+
require 'returnly/refund_calculator'
|
10
|
+
|
11
|
+
require 'returnly/version'
|
12
|
+
require 'returnly/gift_card'
|
13
|
+
require 'returnly/refund/amount_calculator'
|
14
|
+
require 'returnly/refund/return_item_restock_policy'
|
15
|
+
require 'returnly/services/create_reimbursement'
|
16
|
+
require 'returnly/services/mark_items_as_returned'
|
17
|
+
require 'returnly/refund_presenter'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Returnly
|
2
|
+
module Builders
|
3
|
+
class CustomerReturn
|
4
|
+
class << self
|
5
|
+
def build_by_return_items(return_items)
|
6
|
+
Spree::CustomerReturn.create(
|
7
|
+
return_items: return_items,
|
8
|
+
stock_location_id: stock_location_id(return_items.first)
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
def build_by_stock_location(stock_location)
|
13
|
+
Spree::CustomerReturn.new(stock_location: stock_location)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def stock_location_id(return_item)
|
19
|
+
return_item_order(return_item).shipments.last.stock_location_id
|
20
|
+
end
|
21
|
+
|
22
|
+
def return_item_order(return_item)
|
23
|
+
return_item.inventory_unit.order
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Returnly
|
2
|
+
module Builders
|
3
|
+
class ReturnItem
|
4
|
+
attr_accessor :order
|
5
|
+
|
6
|
+
def initialize(order)
|
7
|
+
@order = order
|
8
|
+
end
|
9
|
+
|
10
|
+
def build_by_requested_line_items(requested_line_items)
|
11
|
+
requested_line_items.each_with_object([]) do |request_line_item, return_items|
|
12
|
+
quantity = request_line_item[:units].to_i
|
13
|
+
next return_items unless quantity > 0
|
14
|
+
|
15
|
+
inventory_units_by(request_line_item[:order_line_item_id]).take(quantity).each do |inventory_unit|
|
16
|
+
return_items << build_by_inventory_unit(inventory_unit, request_line_item)
|
17
|
+
end
|
18
|
+
|
19
|
+
return_items
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_by_inventory_unit(inventory_unit, options = {})
|
24
|
+
Spree::ReturnItem.new(
|
25
|
+
amount: inventory_unit.line_item.price,
|
26
|
+
acceptance_status: 'accepted',
|
27
|
+
inventory_unit_id: inventory_unit.id,
|
28
|
+
reception_status_event: 'receive',
|
29
|
+
resellable: options[:restock]
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def inventory_units_by(line_item_ids)
|
36
|
+
order.inventory_units.includes(:line_item)
|
37
|
+
.where(line_item_id: line_item_ids)
|
38
|
+
.where.not(state: 'returned')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Returnly
|
2
|
+
class DiscountCalculator
|
3
|
+
attr_accessor :order
|
4
|
+
|
5
|
+
DISCOUNTER_CLASSES = {
|
6
|
+
'Spree::Order' => Returnly::Discounts::Order,
|
7
|
+
'Spree::LineItem' => Returnly::Discounts::LineItem
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
def initialize(order)
|
11
|
+
@order = order
|
12
|
+
end
|
13
|
+
|
14
|
+
def calculate(line_items = [])
|
15
|
+
line_items.inject(0) do |discount_amount, item|
|
16
|
+
line_item = find_line_item(item[:order_line_item_id])
|
17
|
+
next discount_amount if line_item.nil?
|
18
|
+
|
19
|
+
discount_amount += discount_amount_for(line_item, item[:units].to_i)
|
20
|
+
discount_amount
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def discount_amount_for(line_item, units)
|
27
|
+
discounters.inject(0) do |amount, discounter|
|
28
|
+
amount += discounter.discount_amount(line_item, units)
|
29
|
+
amount
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def discounters
|
34
|
+
order_promotion_adjustments.map { |promotion_adjustment| build_discounter_by(promotion_adjustment) }.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_discounter_by(promotion_adjustment)
|
38
|
+
discounter_class = DISCOUNTER_CLASSES[promotion_adjustment.adjustable_type]
|
39
|
+
return nil if discounter_class.nil?
|
40
|
+
discounter_class.new(order, promotion_adjustment)
|
41
|
+
end
|
42
|
+
|
43
|
+
def order_promotion_adjustments
|
44
|
+
order.all_adjustments.where(source_type: 'Spree::PromotionAction', eligible: true)
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_line_item(id)
|
48
|
+
order.line_items.find_by(id: id)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Returnly
|
2
|
+
module Discounts
|
3
|
+
class LineItem
|
4
|
+
attr_reader :adjustment, :order
|
5
|
+
|
6
|
+
def initialize(order, adjustment)
|
7
|
+
@adjustment = adjustment
|
8
|
+
@order = order
|
9
|
+
end
|
10
|
+
|
11
|
+
def discount_amount(line_item, units = 0)
|
12
|
+
return 0.0 if units <= 0
|
13
|
+
return 0.0 if adjustment.adjustable != line_item
|
14
|
+
|
15
|
+
units = line_item.quantity if units > line_item.quantity
|
16
|
+
adjustment.amount * weight_of(line_item, units.to_d)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def weight_of(line_item, units)
|
22
|
+
(units / line_item.quantity).round(2, :down)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Returnly
|
2
|
+
module Discounts
|
3
|
+
class Order
|
4
|
+
attr_reader :adjustment, :order
|
5
|
+
|
6
|
+
def initialize(order, adjustment)
|
7
|
+
@adjustment = adjustment
|
8
|
+
@order = order
|
9
|
+
end
|
10
|
+
|
11
|
+
def discount_amount(line_item, units = 0)
|
12
|
+
return 0.0 if units <= 0
|
13
|
+
|
14
|
+
units = line_item.quantity if units > line_item.quantity
|
15
|
+
(adjustment.amount * price_percent(line_item) / 100) * weight_of(line_item, units.to_d)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def price_percent(line_item)
|
21
|
+
(line_item.price * 100 / order_items_price).round(2, :down)
|
22
|
+
end
|
23
|
+
|
24
|
+
def order_items_price
|
25
|
+
order.line_items.sum(&:price)
|
26
|
+
end
|
27
|
+
|
28
|
+
def weight_of(line_item, units)
|
29
|
+
(units / line_item.quantity).round(2, :down)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Returnly
|
2
|
+
class << self
|
3
|
+
mattr_accessor :return_item_amount_calculator,
|
4
|
+
:return_item_builder,
|
5
|
+
:return_item_restock_policy,
|
6
|
+
:refunder,
|
7
|
+
:refund_calculator,
|
8
|
+
:refund_presenter,
|
9
|
+
:gift_card,
|
10
|
+
:git_card_estimate
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.configure(&block)
|
14
|
+
yield self if block
|
15
|
+
end
|
16
|
+
|
17
|
+
class Engine < Rails::Engine
|
18
|
+
require 'spree/core'
|
19
|
+
|
20
|
+
engine_name 'returnly'
|
21
|
+
|
22
|
+
config.autoload_paths += %W(#{config.root}/lib)
|
23
|
+
|
24
|
+
# use rspec for tests
|
25
|
+
config.generators do |g|
|
26
|
+
g.test_framework :rspec
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.activate
|
30
|
+
Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
|
31
|
+
Rails.configuration.cache_classes ? require(c) : load(c)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
config.to_prepare(&method(:activate).to_proc)
|
36
|
+
|
37
|
+
rake_tasks do
|
38
|
+
namespace :returnly do
|
39
|
+
require 'securerandom'
|
40
|
+
|
41
|
+
desc "Creates an returnly user and prints the generated API key"
|
42
|
+
task install_user: :environment do
|
43
|
+
|
44
|
+
returnly_email = 'api@returnly.com'
|
45
|
+
|
46
|
+
password = SecureRandom.hex(24)
|
47
|
+
|
48
|
+
if (user = Spree::User.find_by email: returnly_email)
|
49
|
+
puts "The Returnly User is already generated"
|
50
|
+
puts "The Api key is: #{user.spree_api_key}"
|
51
|
+
else
|
52
|
+
user = Spree::User.new email: returnly_email, password: password,
|
53
|
+
password_confirmation: password
|
54
|
+
|
55
|
+
user.generate_spree_api_key!
|
56
|
+
|
57
|
+
if user.save!
|
58
|
+
puts "****************************************************"
|
59
|
+
puts "Api key Generated #{user.spree_api_key}"
|
60
|
+
puts "Returnly User Password #{password}"
|
61
|
+
puts "Returnly User email #{returnly_email}"
|
62
|
+
puts "****************************************************"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
# Define your Spree extensions Factories within this file to enable applications, and other extensions to use and override them.
|
3
|
+
#
|
4
|
+
# Example adding this to your spec_helper will load these Factories for use:
|
5
|
+
# require 'returnly/factories'
|
6
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Returnly
|
2
|
+
module Refund
|
3
|
+
class AmountCalculator
|
4
|
+
attr_accessor :refund
|
5
|
+
|
6
|
+
def initialize(refund)
|
7
|
+
self.refund = refund
|
8
|
+
end
|
9
|
+
|
10
|
+
def return_item_refund_amount(return_item)
|
11
|
+
(item_price(return_item) * item_price_percentage / 100).round(2, :down)
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
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)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|