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,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
@@ -0,0 +1,5 @@
1
+ # Sample localization file for English. Add more files in this directory for other locales.
2
+ # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3
+
4
+ en:
5
+ hello: Hello world
@@ -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
@@ -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,21 @@
1
+ module Returnly
2
+ NilGiftCard = Struct.new(:refund) do
3
+ def code
4
+ nil
5
+ end
6
+
7
+ def id
8
+ nil
9
+ end
10
+ end
11
+
12
+ NilGiftCardEstimate = Struct.new(:order) do
13
+ def code
14
+ nil
15
+ end
16
+
17
+ def id
18
+ nil
19
+ end
20
+ end
21
+ 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