spree_core 2.3.13 → 2.4.0.rc1
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/assets/javascripts/spree.js.coffee.erb +1 -5
- data/app/helpers/spree/base_helper.rb +22 -11
- data/app/helpers/spree/products_helper.rb +8 -7
- data/app/mailers/spree/base_mailer.rb +1 -0
- data/app/mailers/spree/reimbursement_mailer.rb +10 -0
- data/app/mailers/spree/test_mailer.rb +2 -3
- data/app/models/concerns/spree/adjustment_source.rb +24 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +33 -0
- data/app/models/concerns/spree/named_type.rb +12 -0
- data/app/models/concerns/spree/user_address.rb +30 -0
- data/app/models/concerns/spree/user_payment_source.rb +19 -0
- data/app/models/spree/address.rb +13 -6
- data/app/models/spree/adjustment.rb +5 -5
- data/app/models/spree/app_configuration.rb +8 -4
- data/app/models/spree/asset.rb +1 -1
- data/app/models/spree/base.rb +0 -3
- data/app/models/spree/calculator/flat_rate.rb +1 -5
- data/app/models/spree/calculator/returns/default_refund_amount.rb +36 -0
- data/app/models/spree/classification.rb +1 -1
- data/app/models/spree/credit_card.rb +18 -22
- data/app/models/spree/customer_return.rb +70 -0
- data/app/models/spree/exchange.rb +42 -0
- data/app/models/spree/gateway/bogus.rb +3 -3
- data/app/models/spree/image.rb +1 -1
- data/app/models/spree/inventory_unit.rb +32 -8
- data/app/models/spree/item_adjustments.rb +7 -11
- data/app/models/spree/legacy_user.rb +2 -2
- data/app/models/spree/line_item.rb +25 -12
- data/app/models/spree/option_type.rb +1 -1
- data/app/models/spree/option_value.rb +1 -8
- data/app/models/spree/order.rb +163 -145
- data/app/models/spree/order/checkout.rb +35 -23
- data/app/models/spree/order/payments.rb +66 -0
- data/app/models/spree/order_contents.rb +34 -24
- data/app/models/spree/order_populator.rb +6 -4
- data/app/models/spree/order_updater.rb +10 -1
- data/app/models/spree/payment.rb +19 -16
- data/app/models/spree/payment/processing.rb +40 -72
- data/app/models/spree/payment_method.rb +1 -1
- data/app/models/spree/payment_method/check.rb +0 -2
- data/app/models/spree/preference.rb +1 -1
- data/app/models/spree/preferences/preferable.rb +20 -0
- data/app/models/spree/price.rb +13 -3
- data/app/models/spree/product.rb +24 -29
- data/app/models/spree/product_property.rb +0 -2
- data/app/models/spree/promotion.rb +66 -24
- data/app/models/spree/promotion/actions/create_adjustment.rb +2 -2
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +15 -11
- data/app/models/spree/promotion/actions/create_line_items.rb +2 -12
- data/app/models/spree/promotion/rules/first_order.rb +6 -2
- data/app/models/spree/promotion/rules/item_total.rb +42 -4
- data/app/models/spree/promotion/rules/one_use_per_user.rb +24 -0
- data/app/models/spree/promotion/rules/product.rb +13 -11
- data/app/models/spree/promotion/rules/taxon.rb +61 -0
- data/app/models/spree/promotion/rules/user.rb +1 -1
- data/app/models/spree/promotion/rules/user_logged_in.rb +4 -1
- data/app/models/spree/promotion_category.rb +6 -0
- data/app/models/spree/promotion_handler/cart.rb +14 -18
- data/app/models/spree/promotion_handler/coupon.rb +25 -16
- data/app/models/spree/promotion_rule.rb +13 -0
- data/app/models/spree/property.rb +1 -3
- data/app/models/spree/refund.rb +91 -0
- data/app/models/spree/refund_reason.rb +13 -0
- data/app/models/spree/reimbursement.rb +148 -0
- data/app/models/spree/reimbursement/credit.rb +25 -0
- data/app/models/spree/reimbursement/reimbursement_type_engine.rb +56 -0
- data/app/models/spree/reimbursement/reimbursement_type_validator.rb +12 -0
- data/app/models/spree/reimbursement_performer.rb +43 -0
- data/app/models/spree/reimbursement_tax_calculator.rb +38 -0
- data/app/models/spree/reimbursement_type.rb +16 -0
- data/app/models/spree/reimbursement_type/credit.rb +13 -0
- data/app/models/spree/reimbursement_type/exchange.rb +9 -0
- data/app/models/spree/reimbursement_type/original_payment.rb +13 -0
- data/app/models/spree/reimbursement_type/reimbursement_helpers.rb +50 -0
- data/app/models/spree/return_authorization.rb +52 -68
- data/app/models/spree/return_authorization_reason.rb +7 -0
- data/app/models/spree/return_item.rb +230 -0
- data/app/models/spree/return_item/default_eligibility_validator.rb +27 -0
- data/app/models/spree/return_item/eligibility_validator/base_validator.rb +24 -0
- data/app/models/spree/return_item/eligibility_validator/rma_required.rb +17 -0
- data/app/models/spree/return_item/eligibility_validator/time_since_purchase.rb +16 -0
- data/app/models/spree/return_item/exchange_variant_eligibility/same_option_value.rb +34 -0
- data/app/models/spree/return_item/exchange_variant_eligibility/same_product.rb +10 -0
- data/app/models/spree/returns_calculator.rb +8 -0
- data/app/models/spree/shipment.rb +209 -154
- data/app/models/spree/shipment_handler.rb +43 -0
- data/app/models/spree/shipping_calculator.rb +1 -1
- data/app/models/spree/shipping_category.rb +2 -2
- data/app/models/spree/shipping_method.rb +1 -1
- data/app/models/spree/shipping_method_category.rb +1 -1
- data/app/models/spree/shipping_rate.rb +4 -0
- data/app/models/spree/stock/adjuster.rb +10 -11
- data/app/models/spree/stock/availability_validator.rb +6 -10
- data/app/models/spree/stock/content_item.rb +48 -0
- data/app/models/spree/stock/coordinator.rb +14 -7
- data/app/models/spree/stock/estimator.rb +1 -1
- data/app/models/spree/stock/inventory_unit_builder.rb +21 -0
- data/app/models/spree/stock/package.rb +38 -51
- data/app/models/spree/stock/packer.rb +13 -11
- data/app/models/spree/stock/prioritizer.rb +7 -7
- data/app/models/spree/stock/splitter/base.rb +2 -2
- data/app/models/spree/stock_item.rb +6 -9
- data/app/models/spree/stock_location.rb +17 -25
- data/app/models/spree/stock_movement.rb +1 -8
- data/app/models/spree/stock_transfer.rb +0 -2
- data/app/models/spree/store.rb +1 -1
- data/app/models/spree/tax_category.rb +2 -2
- data/app/models/spree/tax_rate.rb +16 -22
- data/app/models/spree/taxon.rb +1 -1
- data/app/models/spree/variant.rb +45 -23
- data/app/models/spree/zone.rb +17 -17
- data/app/models/spree/zone_member.rb +1 -1
- data/app/views/layouts/spree/base_mailer.html.erb +784 -0
- data/app/views/spree/order_mailer/cancel_email.html.erb +45 -0
- data/app/views/spree/order_mailer/cancel_email.text.erb +2 -2
- data/app/views/spree/order_mailer/confirm_email.html.erb +84 -0
- data/app/views/spree/order_mailer/confirm_email.text.erb +2 -2
- data/app/views/spree/reimbursement_mailer/reimbursement_email.text.erb +22 -0
- data/app/views/spree/shared/_base_mailer_footer.html.erb +20 -0
- data/app/views/spree/shared/_base_mailer_header.html.erb +31 -0
- data/app/views/spree/shipment_mailer/shipped_email.html.erb +34 -0
- data/app/views/spree/test_mailer/test_email.html.erb +40 -0
- data/app/views/spree/test_mailer/test_email.text.erb +2 -2
- data/config/initializers/friendly_id.rb +88 -0
- data/config/initializers/premailer_assets.rb +1 -0
- data/config/initializers/user_class_extensions.rb +9 -22
- data/config/locales/en.yml +180 -12
- data/db/default/spree/states.rb +73 -55
- data/db/migrate/20130213191427_create_default_stock.rb +1 -0
- data/db/migrate/20130807024301_upgrade_adjustments.rb +4 -5
- data/db/migrate/20140309033438_create_store_from_preferences.rb +0 -7
- data/db/migrate/20140318191500_create_spree_taxons_promotion_rules.rb +8 -0
- data/db/migrate/20140530024945_move_order_token_from_tokenized_permission.rb +1 -1
- data/db/migrate/20140601011216_set_shipment_total_for_users_upgrading.rb +5 -3
- data/db/migrate/20140625214618_create_spree_refunds.rb +12 -0
- data/db/migrate/20140702140656_create_spree_return_authorization_inventory_unit.rb +12 -0
- data/db/migrate/20140707125621_rename_return_authorization_inventory_unit_to_return_items.rb +5 -0
- data/db/migrate/20140709160534_backfill_line_item_pre_tax_amount.rb +10 -0
- data/db/migrate/20140710041921_recreate_spree_return_authorizations.rb +55 -0
- data/db/migrate/20140710181204_add_amount_fields_to_return_items.rb +7 -0
- data/db/migrate/20140710190048_drop_return_authorization_amount.rb +5 -0
- data/db/migrate/20140713140455_create_spree_return_authorization_reasons.rb +28 -0
- data/db/migrate/20140713140527_create_spree_refund_reasons.rb +14 -0
- data/db/migrate/20140713142214_rename_return_authorization_reason.rb +5 -0
- data/db/migrate/20140715182625_create_spree_promotion_categories.rb +11 -0
- data/db/migrate/20140716204111_drop_received_at_on_return_items.rb +9 -0
- data/db/migrate/20140716212330_add_reception_and_acceptance_status_to_return_items.rb +6 -0
- data/db/migrate/20140717155155_create_default_refund_reason.rb +9 -0
- data/db/migrate/20140717185932_add_default_to_spree_stock_locations.rb +5 -0
- data/db/migrate/20140718133010_create_spree_customer_returns.rb +9 -0
- data/db/migrate/20140718133349_add_customer_return_id_to_return_item.rb +6 -0
- data/db/migrate/20140718195325_create_friendly_id_slugs.rb +15 -0
- data/db/migrate/20140723004419_rename_spree_refund_return_authorization_id.rb +5 -0
- data/db/migrate/20140723152808_increase_return_item_pre_tax_amount_precision.rb +13 -0
- data/db/migrate/20140723214541_copy_product_slugs_to_slug_history.rb +15 -0
- data/db/migrate/20140725131539_create_spree_reimbursements.rb +21 -0
- data/db/migrate/20140728225422_add_promotionable_to_spree_products.rb +5 -0
- data/db/migrate/20140729133613_add_exchange_inventory_unit_foreign_keys.rb +7 -0
- data/db/migrate/20140730155938_add_acceptance_status_errors_to_return_item.rb +5 -0
- data/db/migrate/20140731150017_create_spree_reimbursement_types.rb +20 -0
- data/db/migrate/20140805171035_add_default_to_spree_credit_cards.rb +5 -0
- data/db/migrate/20140805171219_make_existing_credit_cards_default.rb +10 -0
- data/db/migrate/20140806144901_add_type_to_reimbursement_type.rb +9 -0
- data/db/migrate/20140808184039_create_spree_reimbursement_credits.rb +10 -0
- data/db/migrate/20140827170513_add_meta_title_to_spree_products.rb +7 -0
- data/db/migrate/20140924164824_add_code_to_spree_tax_categories.rb +5 -0
- data/db/migrate/20141002191113_add_code_to_spree_shipping_methods.rb +5 -0
- data/db/migrate/20141007230328_add_cancel_audit_fields_to_spree_orders.rb +6 -0
- data/db/migrate/20141009204607_add_store_id_to_orders.rb +8 -0
- data/lib/generators/spree/install/install_generator.rb +7 -3
- data/lib/spree/core.rb +11 -10
- data/lib/spree/core/controller_helpers/common.rb +3 -10
- data/lib/spree/core/controller_helpers/order.rb +15 -12
- data/lib/spree/core/controller_helpers/strong_parameters.rb +9 -9
- data/lib/spree/core/engine.rb +8 -1
- data/lib/spree/core/importer/order.rb +5 -17
- data/lib/spree/core/search/base.rb +1 -1
- data/lib/spree/core/validators/email.rb +1 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/instrumentation.rb +41 -0
- data/lib/spree/migrations.rb +3 -7
- data/lib/spree/money.rb +2 -2
- data/lib/spree/permitted_attributes.rb +10 -9
- data/lib/spree/testing_support/ability_helpers.rb +25 -25
- data/lib/spree/testing_support/authorization_helpers.rb +3 -5
- data/lib/spree/testing_support/capybara_ext.rb +2 -2
- data/lib/spree/testing_support/factories/calculator_factory.rb +0 -8
- data/lib/spree/testing_support/factories/credit_card_factory.rb +1 -1
- data/lib/spree/testing_support/factories/customer_return_factory.rb +31 -0
- data/lib/spree/testing_support/factories/inventory_unit_factory.rb +1 -0
- data/lib/spree/testing_support/factories/line_item_factory.rb +2 -1
- data/lib/spree/testing_support/factories/order_factory.rb +11 -6
- data/lib/spree/testing_support/factories/promotion_category_factory.rb +6 -0
- data/lib/spree/testing_support/factories/promotion_factory.rb +9 -7
- data/lib/spree/testing_support/factories/refund_factory.rb +14 -0
- data/lib/spree/testing_support/factories/reimbursement_factory.rb +16 -0
- data/lib/spree/testing_support/factories/reimbursement_type_factory.rb +7 -0
- data/lib/spree/testing_support/factories/return_authorization_factory.rb +9 -3
- data/lib/spree/testing_support/factories/return_item_factory.rb +10 -0
- data/lib/spree/testing_support/factories/shipment_factory.rb +1 -0
- data/lib/spree/testing_support/factories/shipping_method_factory.rb +3 -2
- data/lib/spree/testing_support/factories/stock_factory.rb +12 -11
- data/lib/spree/testing_support/flash.rb +2 -2
- data/lib/tasks/email.rake +7 -0
- data/lib/tasks/exchanges.rake +70 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_et.js +23 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_eu.js +25 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_hr.js +25 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_ka.js +25 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_ko.js +25 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_my.js +25 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_pt_BR.js +26 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_pt_PT.js +26 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_sl.js +25 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_sv.js +23 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_uk.js +25 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_zh.js +25 -0
- data/vendor/assets/javascripts/jquery.validate/localization/messages_zh_TW.js +26 -0
- metadata +163 -47
- data/app/models/concerns/spree/ransackable_attributes.rb +0 -19
- data/db/migrate/20141021194502_add_state_lock_version_to_order.rb +0 -5
- data/db/migrate/20141101231208_fix_adjustment_order_presence.rb +0 -13
- data/db/migrate/20141105213646_update_classifications_positions.rb +0 -9
- data/db/migrate/20141120135441_add_guest_token_index_to_spree_orders.rb +0 -5
- data/db/migrate/20150515211137_fix_adjustment_order_id.rb +0 -70
- data/lib/spree/core/adjustment_source.rb +0 -26
- data/lib/spree/core/calculated_adjustments.rb +0 -35
- data/lib/spree/core/controller_helpers.rb +0 -20
- data/lib/spree/core/user_address.rb +0 -32
- data/lib/spree/core/user_payment_source.rb +0 -20
- data/lib/spree/localized_number.rb +0 -20
|
@@ -20,6 +20,16 @@ module Spree
|
|
|
20
20
|
raise 'eligible? should be implemented in a sub-class of Spree::PromotionRule'
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# This states if a promotion can be applied to the specified line item
|
|
24
|
+
# It is true by default, but can be overridden by promotion rules to provide conditions
|
|
25
|
+
def actionable?(line_item)
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def eligibility_errors
|
|
30
|
+
@eligibility_errors ||= ActiveModel::Errors.new(self)
|
|
31
|
+
end
|
|
32
|
+
|
|
23
33
|
private
|
|
24
34
|
def unique_per_promotion
|
|
25
35
|
if Spree::PromotionRule.exists?(promotion_id: promotion_id, type: self.class.name)
|
|
@@ -27,5 +37,8 @@ module Spree
|
|
|
27
37
|
end
|
|
28
38
|
end
|
|
29
39
|
|
|
40
|
+
def eligibility_error_message(key, options = {})
|
|
41
|
+
Spree.t(key, Hash[scope: [:eligibility_errors, :messages]].merge(options))
|
|
42
|
+
end
|
|
30
43
|
end
|
|
31
44
|
end
|
|
@@ -11,8 +11,6 @@ module Spree
|
|
|
11
11
|
|
|
12
12
|
after_touch :touch_all_products
|
|
13
13
|
|
|
14
|
-
self.whitelisted_ransackable_attributes = ['presentation']
|
|
15
|
-
|
|
16
14
|
def self.find_all_by_prototype(prototype)
|
|
17
15
|
id = prototype
|
|
18
16
|
id = prototype.id if prototype.class == Prototype
|
|
@@ -23,7 +21,7 @@ module Spree
|
|
|
23
21
|
private
|
|
24
22
|
|
|
25
23
|
def touch_all_products
|
|
26
|
-
products.
|
|
24
|
+
products.update_all(updated_at: Time.current)
|
|
27
25
|
end
|
|
28
26
|
end
|
|
29
27
|
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class Refund < Spree::Base
|
|
3
|
+
belongs_to :payment, inverse_of: :refunds
|
|
4
|
+
belongs_to :reason, class_name: 'Spree::RefundReason', foreign_key: :refund_reason_id
|
|
5
|
+
belongs_to :reimbursement, inverse_of: :refunds
|
|
6
|
+
|
|
7
|
+
has_many :log_entries, as: :source
|
|
8
|
+
|
|
9
|
+
validates :payment, presence: true
|
|
10
|
+
validates :reason, presence: true
|
|
11
|
+
validates :transaction_id, presence: true, on: :update # can't require this on create because the before_create needs to run first
|
|
12
|
+
validates :amount, presence: true, numericality: {greater_than: 0}
|
|
13
|
+
|
|
14
|
+
validate :check_payment_environment, on: :create, if: :payment
|
|
15
|
+
validate :amount_is_less_than_or_equal_to_allowed_amount, on: :create
|
|
16
|
+
|
|
17
|
+
after_create :perform!
|
|
18
|
+
after_create :create_log_entry
|
|
19
|
+
|
|
20
|
+
scope :non_reimbursement, -> { where(reimbursement_id: nil) }
|
|
21
|
+
|
|
22
|
+
def money
|
|
23
|
+
Spree::Money.new(amount, { currency: payment.currency })
|
|
24
|
+
end
|
|
25
|
+
alias display_amount money
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def total_amount_reimbursed_for(reimbursement)
|
|
29
|
+
reimbursement.refunds.to_a.sum(&:amount)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def description
|
|
34
|
+
payment.payment_method.name
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# attempts to perform the refund.
|
|
40
|
+
# raises an error if the refund fails.
|
|
41
|
+
def perform!
|
|
42
|
+
return true if transaction_id.present?
|
|
43
|
+
|
|
44
|
+
credit_cents = Spree::Money.new(amount.to_f, currency: payment.currency).money.cents
|
|
45
|
+
|
|
46
|
+
@response = process!(credit_cents)
|
|
47
|
+
|
|
48
|
+
self.transaction_id = @response.authorization
|
|
49
|
+
update_columns(transaction_id: transaction_id)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# return an activemerchant response object if successful or else raise an error
|
|
53
|
+
def process!(credit_cents)
|
|
54
|
+
response = if payment.payment_method.payment_profiles_supported?
|
|
55
|
+
payment.payment_method.credit(credit_cents, payment.source, payment.transaction_id, {originator: self})
|
|
56
|
+
else
|
|
57
|
+
payment.payment_method.credit(credit_cents, payment.transaction_id, {originator: self})
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if !response.success?
|
|
61
|
+
logger.error(Spree.t(:gateway_error) + " #{response.to_yaml}")
|
|
62
|
+
text = response.params['message'] || response.params['response_reason_text'] || response.message
|
|
63
|
+
raise Core::GatewayError.new(text)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
response
|
|
67
|
+
rescue ActiveMerchant::ConnectionError => e
|
|
68
|
+
logger.error(Spree.t(:gateway_error) + " #{e.inspect}")
|
|
69
|
+
raise Core::GatewayError.new(Spree.t(:unable_to_connect_to_gateway))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Saftey check to make sure we're not accidentally performing operations on a live gateway.
|
|
73
|
+
# Ex. When testing in staging environment with a copy of production data.
|
|
74
|
+
def check_payment_environment
|
|
75
|
+
if payment.payment_method.environment != Rails.env
|
|
76
|
+
message = Spree.t(:gateway_config_unavailable) + " - #{Rails.env}"
|
|
77
|
+
errors.add(:base, message)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def create_log_entry
|
|
82
|
+
log_entries.create!(details: @response.to_yaml)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def amount_is_less_than_or_equal_to_allowed_amount
|
|
86
|
+
if amount > payment.credit_allowed
|
|
87
|
+
errors.add(:amount, :greater_than_allowed)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class RefundReason < Spree::Base
|
|
3
|
+
include Spree::NamedType
|
|
4
|
+
|
|
5
|
+
RETURN_PROCESSING_REASON = 'Return processing'
|
|
6
|
+
|
|
7
|
+
has_many :refunds
|
|
8
|
+
|
|
9
|
+
def self.return_processing_reason
|
|
10
|
+
find_by!(name: RETURN_PROCESSING_REASON, mutable: false)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class Reimbursement < Spree::Base
|
|
3
|
+
class IncompleteReimbursement < StandardError; end
|
|
4
|
+
|
|
5
|
+
belongs_to :order, inverse_of: :reimbursements
|
|
6
|
+
belongs_to :customer_return, inverse_of: :reimbursements, touch: true
|
|
7
|
+
|
|
8
|
+
has_many :refunds, inverse_of: :reimbursement
|
|
9
|
+
has_many :credits, inverse_of: :reimbursement, class_name: 'Spree::Reimbursement::Credit'
|
|
10
|
+
|
|
11
|
+
has_many :return_items, inverse_of: :reimbursement
|
|
12
|
+
|
|
13
|
+
validates :order, presence: true
|
|
14
|
+
validate :validate_return_items_belong_to_same_order
|
|
15
|
+
|
|
16
|
+
accepts_nested_attributes_for :return_items, allow_destroy: true
|
|
17
|
+
|
|
18
|
+
before_create :generate_number
|
|
19
|
+
|
|
20
|
+
# The reimbursement_tax_calculator property should be set to an object that responds to "call"
|
|
21
|
+
# and accepts a reimbursement object. Invoking "call" should update the tax fields on the
|
|
22
|
+
# associated ReturnItems.
|
|
23
|
+
# This allows a store to easily integrate with third party tax services.
|
|
24
|
+
class_attribute :reimbursement_tax_calculator
|
|
25
|
+
self.reimbursement_tax_calculator = ReimbursementTaxCalculator
|
|
26
|
+
# A separate attribute here allows you to use a more performant calculator for estimates
|
|
27
|
+
# and a different one (e.g. one that hits a 3rd party API) for the final caluclations.
|
|
28
|
+
class_attribute :reimbursement_simulator_tax_calculator
|
|
29
|
+
self.reimbursement_simulator_tax_calculator = ReimbursementTaxCalculator
|
|
30
|
+
|
|
31
|
+
# The reimbursement_models property should contain an array of all models that provide
|
|
32
|
+
# reimbursements.
|
|
33
|
+
# This allows a store to incorporate custom reimbursement methods that Spree doesn't know about.
|
|
34
|
+
# Each model must implement a "total_amount_reimbursed_for" method.
|
|
35
|
+
# Example:
|
|
36
|
+
# Refund.total_amount_reimbursed_for(reimbursement)
|
|
37
|
+
# See the `reimbursement_generator` property regarding the generation of custom reimbursements.
|
|
38
|
+
class_attribute :reimbursement_models
|
|
39
|
+
self.reimbursement_models = [Refund]
|
|
40
|
+
|
|
41
|
+
# The reimbursement_performer property should be set to an object that responds to the following methods:
|
|
42
|
+
# - #perform
|
|
43
|
+
# - #simulate
|
|
44
|
+
# see ReimbursementPerformer for details.
|
|
45
|
+
# This allows a store to customize their reimbursement methods and logic.
|
|
46
|
+
class_attribute :reimbursement_performer
|
|
47
|
+
self.reimbursement_performer = ReimbursementPerformer
|
|
48
|
+
|
|
49
|
+
# These are called if the call to "reimburse!" succeeds.
|
|
50
|
+
class_attribute :reimbursement_success_hooks
|
|
51
|
+
self.reimbursement_success_hooks = []
|
|
52
|
+
|
|
53
|
+
# These are called if the call to "reimburse!" fails.
|
|
54
|
+
class_attribute :reimbursement_failure_hooks
|
|
55
|
+
self.reimbursement_failure_hooks = []
|
|
56
|
+
|
|
57
|
+
state_machine :reimbursement_status, initial: :pending do
|
|
58
|
+
after_transition to: :reimbursed, do: :send_reimbursement_email
|
|
59
|
+
|
|
60
|
+
event :errored do
|
|
61
|
+
transition to: :errored, from: :pending
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
event :reimbursed do
|
|
65
|
+
transition to: :reimbursed, from: [:pending, :errored]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class << self
|
|
71
|
+
def build_from_customer_return(customer_return)
|
|
72
|
+
order = customer_return.order
|
|
73
|
+
order.reimbursements.build({
|
|
74
|
+
customer_return: customer_return,
|
|
75
|
+
return_items: customer_return.return_items.accepted.not_reimbursed,
|
|
76
|
+
})
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def display_total
|
|
81
|
+
Spree::Money.new(total, { currency: order.currency })
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def calculated_total
|
|
85
|
+
# rounding down to handle edge cases for consecutive partial returns where rounding
|
|
86
|
+
# might cause us to try to reimburse more than was originally billed
|
|
87
|
+
return_items.to_a.sum(&:total).to_d.round(2, :down)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def paid_amount
|
|
91
|
+
reimbursement_models.sum do |model|
|
|
92
|
+
model.total_amount_reimbursed_for(self)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def unpaid_amount
|
|
97
|
+
total - paid_amount
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def perform!
|
|
101
|
+
reimbursement_tax_calculator.call(self)
|
|
102
|
+
reload
|
|
103
|
+
update!(total: calculated_total)
|
|
104
|
+
|
|
105
|
+
reimbursement_performer.perform(self)
|
|
106
|
+
|
|
107
|
+
if unpaid_amount.zero?
|
|
108
|
+
reimbursed!
|
|
109
|
+
reimbursement_success_hooks.each { |h| h.call self }
|
|
110
|
+
else
|
|
111
|
+
errored!
|
|
112
|
+
reimbursement_failure_hooks.each { |h| h.call self }
|
|
113
|
+
raise IncompleteReimbursement, Spree.t("validation.unpaid_amount_not_zero", amount: unpaid_amount)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def simulate
|
|
118
|
+
reimbursement_simulator_tax_calculator.call(self)
|
|
119
|
+
reload
|
|
120
|
+
update!(total: calculated_total)
|
|
121
|
+
|
|
122
|
+
reimbursement_performer.simulate(self)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def return_items_requiring_exchange
|
|
126
|
+
return_items.select(&:exchange_required?)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def generate_number
|
|
132
|
+
self.number ||= loop do
|
|
133
|
+
random = "RI#{Array.new(9){rand(9)}.join}"
|
|
134
|
+
break random unless self.class.exists?(number: random)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def validate_return_items_belong_to_same_order
|
|
139
|
+
if return_items.any? { |ri| ri.inventory_unit.order_id != order_id }
|
|
140
|
+
errors.add(:base, :return_items_order_id_does_not_match)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def send_reimbursement_email
|
|
145
|
+
Spree::ReimbursementMailer.reimbursement_email(self.id).deliver
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class Reimbursement::Credit < Spree::Base
|
|
3
|
+
class_attribute :default_creditable_class
|
|
4
|
+
self.default_creditable_class = nil
|
|
5
|
+
|
|
6
|
+
belongs_to :reimbursement, inverse_of: :credits
|
|
7
|
+
belongs_to :creditable, polymorphic: true
|
|
8
|
+
|
|
9
|
+
validates :creditable, presence: true
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def total_amount_reimbursed_for(reimbursement)
|
|
13
|
+
reimbursement.credits.to_a.sum(&:amount)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def description
|
|
18
|
+
creditable.class.name.demodulize
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def display_amount
|
|
22
|
+
Spree::Money.new(amount, { currency: creditable.try(:currency) || "USD" })
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class Reimbursement::ReimbursementTypeEngine
|
|
3
|
+
include Spree::Reimbursement::ReimbursementTypeValidator
|
|
4
|
+
|
|
5
|
+
class_attribute :refund_time_constraint
|
|
6
|
+
self.refund_time_constraint = 90.days
|
|
7
|
+
|
|
8
|
+
class_attribute :default_reimbursement_type
|
|
9
|
+
self.default_reimbursement_type = Spree::ReimbursementType::OriginalPayment
|
|
10
|
+
|
|
11
|
+
class_attribute :expired_reimbursement_type
|
|
12
|
+
self.expired_reimbursement_type = Spree::ReimbursementType::OriginalPayment
|
|
13
|
+
|
|
14
|
+
class_attribute :exchange_reimbursement_type
|
|
15
|
+
self.exchange_reimbursement_type = Spree::ReimbursementType::Exchange
|
|
16
|
+
|
|
17
|
+
def initialize(return_items)
|
|
18
|
+
@return_items = return_items
|
|
19
|
+
@reimbursement_type_hash = Hash.new {|h,k| h[k] = Array.new }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def calculate_reimbursement_types
|
|
23
|
+
@return_items.each do |return_item|
|
|
24
|
+
reimbursement_type = calculate_reimbursement_type(return_item)
|
|
25
|
+
add_reimbursement_type(return_item, reimbursement_type)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@reimbursement_type_hash
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def calculate_reimbursement_type(return_item)
|
|
34
|
+
if return_item.exchange_required?
|
|
35
|
+
exchange_reimbursement_type
|
|
36
|
+
elsif return_item.override_reimbursement_type.present?
|
|
37
|
+
return_item.override_reimbursement_type.class
|
|
38
|
+
elsif return_item.preferred_reimbursement_type.present?
|
|
39
|
+
if valid_preferred_reimbursement_type?(return_item)
|
|
40
|
+
return_item.preferred_reimbursement_type.class
|
|
41
|
+
else
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
elsif past_reimbursable_time_period?(return_item)
|
|
45
|
+
expired_reimbursement_type
|
|
46
|
+
else
|
|
47
|
+
default_reimbursement_type
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_reimbursement_type(return_item, reimbursement_type)
|
|
52
|
+
return unless reimbursement_type
|
|
53
|
+
@reimbursement_type_hash[reimbursement_type] << return_item
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Reimbursement::ReimbursementTypeValidator
|
|
3
|
+
def valid_preferred_reimbursement_type?(return_item)
|
|
4
|
+
!past_reimbursable_time_period?(return_item) || return_item.preferred_reimbursement_type == expired_reimbursement_type
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def past_reimbursable_time_period?(return_item)
|
|
8
|
+
shipped_at = return_item.inventory_unit.shipment.shipped_at
|
|
9
|
+
shipped_at && shipped_at < refund_time_constraint.ago
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
|
|
3
|
+
class ReimbursementPerformer
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
class_attribute :reimbursement_type_engine
|
|
7
|
+
self.reimbursement_type_engine = Spree::Reimbursement::ReimbursementTypeEngine
|
|
8
|
+
|
|
9
|
+
# Simulate performing the reimbursement without actually saving anything or refunding money, etc.
|
|
10
|
+
# This must return an array of objects that respond to the following methods:
|
|
11
|
+
# - #description
|
|
12
|
+
# - #display_amount
|
|
13
|
+
# so they can be displayed in the Admin UI appropriately.
|
|
14
|
+
def simulate(reimbursement)
|
|
15
|
+
execute(reimbursement, true)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Actually perform the reimbursement
|
|
19
|
+
def perform(reimbursement)
|
|
20
|
+
execute(reimbursement, false)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def execute(reimbursement, simulate)
|
|
26
|
+
reimbursement_type_hash = calculate_reimbursement_types(reimbursement)
|
|
27
|
+
|
|
28
|
+
reimbursement_type_hash.flat_map do |reimbursement_type, return_items|
|
|
29
|
+
reimbursement_type.reimburse(reimbursement, return_items, simulate)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def calculate_reimbursement_types(reimbursement)
|
|
34
|
+
# Engine returns hash of preferred reimbursement types pointing at return items
|
|
35
|
+
# {Spree::ReimbursementType::OriginalPayment => [ReturnItem, ...], Spree::ReimbursementType::Exchange => [ReturnItem, ...]}
|
|
36
|
+
reimbursement_type_engine.new(reimbursement.return_items).calculate_reimbursement_types
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
|
|
3
|
+
# Tax calculation is broken out at this level to allow easy integration with 3rd party
|
|
4
|
+
# taxation systems. Those systems are usually geared toward calculating all items at once
|
|
5
|
+
# rather than one at a time.
|
|
6
|
+
#
|
|
7
|
+
# To use an alternative tax calculator do this:
|
|
8
|
+
# Spree::ReturnAuthorization.reimbursement_tax_calculator = calculator_object
|
|
9
|
+
# where `calculator_object` is an object that responds to "call" and accepts a reimbursement object
|
|
10
|
+
|
|
11
|
+
class ReimbursementTaxCalculator
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
|
|
15
|
+
def call(reimbursement)
|
|
16
|
+
reimbursement.return_items.includes(:inventory_unit).each do |return_item|
|
|
17
|
+
set_tax!(return_item)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def set_tax!(return_item)
|
|
24
|
+
percent_of_tax = (return_item.pre_tax_amount <= 0) ? 0 : return_item.pre_tax_amount / Spree::ReturnItem.refund_amount_calculator.new.compute(return_item)
|
|
25
|
+
|
|
26
|
+
additional_tax_total = percent_of_tax * return_item.inventory_unit.additional_tax_total
|
|
27
|
+
included_tax_total = percent_of_tax * return_item.inventory_unit.included_tax_total
|
|
28
|
+
|
|
29
|
+
return_item.update_attributes!({
|
|
30
|
+
additional_tax_total: additional_tax_total,
|
|
31
|
+
included_tax_total: included_tax_total,
|
|
32
|
+
})
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|