Created
April 15, 2015 14:56
-
-
Save zaeem/10da5c8ca8f8ba836e8f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Cart < ActiveRecord::Base | |
include ModelPlatform | |
include Workflow | |
class CartAddError < StandardError; end | |
#Define which of the classes in the Carts module are enabled | |
def self.available_carts | |
@@available_carts ||= [ | |
Carts::DsrCart, | |
Carts::DsrCustomerCart, | |
Carts::DsrCustomerPartyCart, | |
Carts::DsrHostessCart, | |
Carts::DsrHostessPartyCart, | |
Carts::HostessCart, | |
Carts::HostessPartyCart, | |
Carts::StarterKitCart, | |
Carts::Ecom::CustomerDsrCart, | |
Carts::Ecom::PartyCart, | |
Carts::Ecom::CorpCart, | |
] | |
end | |
def self.ecom_available_carts | |
@@ecom_cart_types ||= [ | |
Carts::Ecom::CustomerDsrCart, | |
Carts::Ecom::PartyCart, | |
Carts::Ecom::CorpCart, | |
] | |
end | |
def self.type_to_class(carts = available_carts) | |
carts.inject({}){|hash, klass| hash[klass.type_code] = klass; hash} | |
end | |
# Factory method to instantiate proper Cart class from type_code | |
def self.sti_class(type_code) | |
type_to_class[type_code] || self | |
end | |
#Available cart types, used to add user friendly validation for type_code | |
def available_cart_types | |
Cart.available_carts.map(&:type_code) | |
end | |
def self.type_name | |
I18n.t("cart.type.#{type_code}") | |
end | |
def self.type_public_name | |
I18n.t("cart.type_public_name.#{type_code}") | |
end | |
#Check that cart is not temporary disabled | |
def self.type_enabled?(type_code) | |
sti_class(type_code).enabled? | |
end | |
def self.enabled? | |
true | |
end | |
def ecom? | |
false | |
end | |
def should_populate_shipping_data? | |
false | |
end | |
# https://stelladot.jira.com/wiki/display/KEEP/Order+-+Statuses | |
STATE_TO_NAME = { | |
active: "Active", | |
abandoned: "Abandoned", | |
convert_pending: "Convert Pending", | |
pending_removal: "Pending Removal", | |
checkout_in_progress: "In Progress", | |
imported: "Imported" | |
} | |
#State machine | |
workflow do | |
state :active do | |
event :pending_removal, transitions_to: :pending_removal | |
event :checkout_in_progress, transitions_to: :checkout_in_progress | |
event :abandon, transitions_to: :abandoned | |
event :convert, transitions_to: :convert_pending | |
event :active, transitions_to: :active | |
end | |
state :abandoned | |
state :convert_pending do | |
event :pending_removal, transitions_to: :pending_removal | |
event :import, transitions_to: :imported | |
event :active, transitions_to: :active | |
end | |
state :pending_removal do | |
event :active, transitions_to: :active | |
end | |
state :checkout_in_progress do | |
event :convert, transitions_to: :convert_pending | |
event :active, transitions_to: :active | |
end | |
state :imported | |
end | |
#Scopes | |
scope :active ,-> {where("workflow_state = ?", :active)} | |
scope :editable,-> {where("workflow_state = ? || workflow_state = ?", :active, :convert_pending)} | |
scope :working, -> {where("workflow_state IN(?)",["active", "convert_pending", "checkout_in_progress"])} | |
scope :available, -> {where("workflow_state IN(?)",["active", "convert_pending", "checkout_in_progress", "imported"])} | |
scope :from_customer, ->(customer_id){where(customer_id: customer_id)} | |
scope :exists_from_params, ->(params){where(customer_id: params[:customer_id])} | |
scope :not_imported, -> {where(workflow_state: ["convert_pending"])} | |
scope :imported, -> {where(workflow_state: "imported")} | |
validates :type_code, inclusion: {in: :available_cart_types} | |
validate :all_party_fields_updated | |
validates :customer_id, presence: true, if: :force_customer_on_save? | |
validates :dsr_id, presence: true, if: :force_dsr_on_save? | |
with_options unless: :skip_customer_validation? do |cart| | |
cart.validates :customer_first_name, :customer_last_name, :customer_email, presence: true | |
cart.validates :customer_email, email_format:{message:"The email address you entered is invalid. Please enter a valid email address"} | |
end | |
validate :validate_promotions, if: :active? | |
after_validation do | |
#Bug with validates_email_format_of (:customer_email key is "not included" but still present in keys) | |
errors.delete(:customer_email) if errors.keys.include?(:customer_email) && !errors.has_key?(:customer_email) | |
end | |
#Money attributes | |
monetize :shipping_fee_tax_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :total_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :item_total_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :tax_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :shipping_total_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :shipping_rate_fee_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :store_credits_total_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :subtotal_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :dsr_discount_total_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :eligible_item_total_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :hostess_credits_total_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :product_credits_total_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :total_prv_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :total_pcv_cents, with_model_currency: :currency, :allow_nil => true, :numericality => {:greater_than_or_equal_to => 0} | |
monetize :promotional_shipping_fee_cents, with_model_currency: :currency, :numericality => {:greater_than_or_equal_to => 0} | |
#Associations | |
has_many :cart_items, dependent: :delete_all, inverse_of: :cart | |
has_many :addresses, dependent: :delete_all | |
has_one :shipping_address, class_name: ShippingAddress, inverse_of: :cart, dependent: :destroy | |
has_one :billing_address, class_name: BillingAddress, inverse_of: :cart, dependent: :destroy | |
has_one :payment_token, -> { order 'id DESC' }, dependent: :delete | |
has_many :transactions, inverse_of: :cart, class_name: RosiPayment::TokenTransaction | |
has_many :payment_auths, dependent: :delete_all | |
accepts_nested_attributes_for :shipping_address | |
accepts_nested_attributes_for :billing_address | |
## CALLBACKS | |
before_save :check_for_billing_address | |
after_initialize :set_default_shipping_rate | |
before_create :set_marketing_email_info | |
before_update :assign_receive_marketing_emails_at | |
before_update :check_for_cart_type_changes | |
def self.type_code() nil; end | |
attr_writer :type_code | |
delegate :type_code, :type_name, :type_public_name, to: :class | |
def public_status | |
STATE_TO_NAME[workflow_state.to_sym] | |
end | |
def set_default_shipping_rate | |
if new_record? | |
self.shipping_rate = ShippingRate.standard | |
end | |
end | |
def check_for_billing_address | |
self.billing_address_as_shipping = (self.shipping_address && self.billing_address && self.shipping_address.equal?(self.billing_address)) | |
return true | |
end | |
def check_for_cart_type_changes | |
if type_changed? | |
cart_items.each do |item| | |
# update item pxx | |
item.save | |
end | |
recalculate_total_prv | |
recalculate_total_pcv | |
recalculate_pqv | |
end | |
end | |
## ECOMMERCE METHODS | |
def self.find_or_create_guest_cart(token,scope=:active) | |
return unless token.present? | |
cart = find_guest_cart(token,scope) | |
unless cart | |
cart = Carts::Ecom::CorpCart.create(guest_token: token) | |
end | |
cart | |
end | |
def self.find_guest_cart(token,scope=:active) | |
return unless token.present? | |
Cart.send(scope).where(guest_token: token).last | |
end | |
## INSTANCE METHODS | |
def referrer | |
@referrer ||= referrer_id && UserService.get_user(referrer_id) | |
end | |
def shipping_rate | |
@shipping_rate ||= ShippingRate.new({ | |
name: self.shipping_rate_name, | |
key: self.shipping_rate_key, | |
fee_cents: self.shipping_rate_fee.cents | |
}) | |
end | |
def shipping_rate=(rate) | |
self.shipping_rate_fee_cents = rate.fee.cents | |
self.shipping_rate_key = rate.key | |
self.shipping_rate_name = rate.name | |
end | |
def set_billing_address_from_cybersource(params) | |
address_from_cybersource = BillingAddress.new | |
address_from_cybersource.set_from_cybersource(params) | |
self.billing_address = address_from_cybersource | |
self.billing_address_as_shipping = (self.shipping_address && self.shipping_address.equal?(address_from_cybersource)) | |
self.save | |
end | |
#Check that all party attributes are updated when party_id changes | |
def all_party_fields_updated | |
if party_id_changed? | |
message = "should change when party_id is updated" | |
errors.add :party_name, message unless party_name_changed? | |
errors.add :party_at, message unless party_at_changed? | |
end | |
end | |
def update_attributes_with_user(cart_params, current_user) | |
self.update_attributes(cart_params) | |
end | |
def party_name=(name) | |
write_attribute(:party_name,name) | |
#Mark as changed? even if the same value | |
party_name_will_change! unless party_name_changed? | |
end | |
def party_at=(date) | |
write_attribute(:party_at,date) | |
#Mark as changed? even if the same value | |
self.party_at_will_change! unless party_at_changed? | |
end | |
def last_transaction | |
self.transactions.order(created_at: :asc).last | |
end | |
def set_errors_from_transaction(tran) | |
unless tran.successful? | |
self.set_user_error_message(tran.reason_code) | |
if tran.error_fields.blank? | |
self.errors.add(:base, tran.message) | |
else | |
fields = tran.error_fields.split(",") | |
fields.each{|field| self.errors.add(field,tran.message)} | |
end | |
end | |
end | |
def set_user_error_message(reason_code) | |
case reason_code.to_i | |
when 202 | |
message = "This card is expired. Please use a different credit card." | |
errors.add(:card_expiry_date, message) | |
when 211, 230 | |
message = "Please check that the card verification number (the security number on the back of the card) is correct." | |
errors.add(:card_cvn, message) | |
when 200, 450..461 | |
message = "Please check that the billing address is correct for the credit card you are using." | |
errors.add(:card_number, message) | |
when 201 | |
message = "There has been an issue processing this card. Please contact the issuing bank." | |
errors.add(:card_number, message) | |
end | |
end | |
def set_errors_from_auth(auth) | |
self.set_user_error_message(auth.reason_code) unless auth.successful? | |
errors.add(:base, auth.message) unless auth.successful? | |
end | |
def update_store_credits(amount) | |
self.store_credits_total_cents = amount | |
update_totals(skip_tax_recalculation: true) | |
end | |
def set_hostess_credits(amount) | |
errors.add(:hostess_credits_total_cents, "You can't apply a hostess promotion to this order") | |
return false | |
end | |
# Only DSR carts perform actions here. | |
def reset_product_credits | |
end | |
def set_product_credits(amount) | |
errors.add(:product_credits_total_cents, "You can't apply product_credits to this order") | |
return false | |
end | |
def supported_discount_types | |
[] | |
end | |
def supports_discount?(key) | |
supported_discount_types.include? key | |
end | |
def customer | |
@customer ||= User.new(id:customer_id) | |
end | |
def customer_data | |
@customer_data ||= customer.reload_user | |
end | |
def dsr_discount_upper_band? | |
false | |
end | |
def dsr_discount_lower_band? | |
false | |
end | |
def set_dsr_discount_percentage | |
end | |
#If item is provided, it calculates the available_balance with that item | |
def available_balance(type,items=[]) | |
#Reject items with same id (and adds it back) to include the last version | |
items = items.empty? ? cart_items : (cart_items - items) + items | |
balance = case type | |
when "hostess_credits" then | |
calculate_hostess_credits_total(items).cents | |
when "product_credits" then | |
calculate_product_credits_total(items).cents | |
when "hostess_discounts" then hostess_discounts_total(items) | |
when "store_credits" then store_credits_total.cents | |
end | |
customer.available_balance(type) - balance | |
end | |
add_method_tracer :available_balance, 'Custom/Cart/available_balance' | |
def update_cart_from_cybersource_response(payment_attributes,address_attributes) | |
payment_token = PaymentToken.new(payment_attributes) | |
payment_token.cart_id = self.id | |
payment_token.save! | |
unless self.billing_address.present? | |
self.set_billing_address_from_cybersource(address_attributes) | |
end | |
send_cart_to_oms = false | |
self.with_lock do | |
if self.eligible_for_checkout? | |
self.checkout_in_progress! | |
send_cart_to_oms = true | |
end | |
end | |
if send_cart_to_oms | |
RosiProducer.cart_checkout_queue.send_message("checkout", self.id) | |
elsif !self.checkout_in_progress? | |
#In the rear case of two attempts to try to checkout | |
#do not roll it back if the checkout is in progress | |
self.rollback_checkout | |
end | |
end | |
def checkout_completed? | |
if !self.convert_pending? && !self.imported? | |
#Do not set errors if checkout_in_progress or if | |
#last payment_token transaction was not replied yet | |
auth = self.last_transaction | |
return false if self.checkout_in_progress? || | |
(auth && auth.successful.nil?) | |
#Set eligible for checkout errors if any | |
self.eligible_for_checkout? | |
#Set errors from Payment Token | |
self.set_errors_from_transaction(auth) if auth | |
#If the above failed, auths would have not been done | |
return false if auth.nil? || !errors.empty? | |
#Set errors from Authorizations | |
for klass in PaymentAuth.auth_types + [Auth::CreditCardAuth] | |
auth = klass.where(cart_id:self.id).last | |
if auth && auth.created_at >= last_transaction.created_at | |
self.set_errors_from_auth(auth) if auth | |
end | |
end | |
return false | |
end | |
return true | |
end | |
def log_event(message,event,action="") | |
full_message = "cart_id=#{self.id} public_order_id=#{self.public_order_id} #{message} event=#{event}" | |
full_message += " action=#{action}" unless action.blank? | |
Rails.logger.info(full_message) | |
end | |
def checkout | |
if !authorize_credit_card || !authorize_credits | |
rollback_checkout | |
return false | |
end | |
#Cart successfully placed | |
self.update_attributes({placed_at: Time.now}) | |
RosiProducer.oms_queue.send_message("convert_cart", self.oms_serializable_hash) | |
log_event("", "checkout", "cart_enqued") | |
return convert! | |
end | |
def rollback_checkout | |
#TODO: What to do if auth is successful but User#debit not? | |
#We can do AuthReversal or avoid doing auth next time | |
InventoryService.release(self.id) | |
self.active! | |
end | |
def cybersource_order_id | |
Rails.env.production? ? self.public_order_id : "#{Rails.env}_#{self.public_order_id}_#{Time.now.to_i}" | |
end | |
def public_order_id | |
read_attribute(:public_order_id) || id | |
end | |
# TODO: avoid querying CatalogService for determining a sku code from its id, if the sku is already in the cart. This can't be implemented (without breaking tests) until the Cart model specs don't use any stubs. | |
def set_items(sku_id,quantity) | |
Rails.logger.info_with_event "cart_id=#{self.id} public_order_id=#{self.public_order_id} sku_id=#{sku_id} quantity=#{quantity}", "cart", "update_cart" | |
quantity = quantity.to_i | |
if validate_quantity_and_item(sku_id, quantity, :skip_items_count) && validate_stock(CatalogService.get_sku(sku_id).code, quantity) | |
old_quantity = cart_items.with_sku(sku_id).no_bundle.count | |
if quantity > old_quantity | |
return import_items(sku_id, quantity - old_quantity) | |
elsif quantity < old_quantity | |
return remove_items(sku_id, old_quantity - quantity) | |
else | |
# do nothing if quantity == old_quantity | |
return true | |
end | |
end | |
return false | |
end | |
def add_bundle(code, quantity, sku_ids) | |
quantity = quantity.to_i | |
Rails.logger.info_with_event "cart_id=#{self.id} public_order_id=#{self.public_order_id} bundle=#{code} quantity=#{quantity}", "cart", "add_to_cart" | |
return false unless validate_quantity(quantity) | |
transaction do | |
lock! | |
quantity.times do # We create a new Bundle for each qty | |
Bundle.create_bundle_with_new_items(code, sku_ids) do |item| | |
item.cart_id = self.id | |
if validate_item(item) && | |
validate_within_product_limit(item, quantity, nil) && | |
validate_stock(item.sku_code, quantity + cart_items.with_sku(item.sku_id).count) | |
item.save! | |
else | |
raise CartAddError | |
end | |
end | |
end | |
update_totals | |
end | |
return true | |
rescue CartAddError | |
return false | |
rescue ActiveRecord::RecordInvalid => e # Normally raised by Bundle.create*, but possibly by item.save! | |
e.record.errors.messages.each { |f, msgs| msgs.each { |m| self.errors.add(:base, m) } } | |
return false | |
end | |
def remove_bundle(bundle_id) | |
valid_ids = cart_items.pluck(:bundle_id).compact.uniq | |
return false unless valid_ids.include?(bundle_id.to_i) | |
Bundle.find(bundle_id).destroy | |
reset_product_credits | |
cart_items(true) | |
update_totals | |
end | |
def add_items(sku_id,quantity) | |
Rails.logger.info_with_event "cart_id=#{self.id} public_order_id=#{self.public_order_id} sku_id=#{sku_id} quantity=#{quantity}", "cart", "add_to_cart" | |
quantity = 1 unless quantity | |
quantity = quantity.to_i | |
if validate_quantity_and_item(sku_id,quantity) && validate_stock(CatalogService.get_sku(sku_id).code, quantity + cart_items.with_sku(sku_id).count) | |
#CartItem#import inserts multiple cart_items in one query | |
return import_items(sku_id,quantity) | |
end | |
#Invalid quantity | |
return false | |
end | |
def remove_items(sku_id,quantity) | |
if quantity | |
quantity = quantity.to_i | |
if validate_quantity(quantity) | |
cart_items_ids = cart_items.with_sku(sku_id).no_bundle.limit(quantity).pluck(:id) | |
#Check if there are enough items to delete | |
if cart_items_ids.size < quantity | |
errors.add :base, "quantity is greater than current items" | |
return false | |
end | |
#Delete items | |
cart_items.where(id:cart_items_ids).delete_all | |
reset_product_credits | |
cart_items(true) | |
return update_totals | |
end | |
#Invalid quantity | |
return false | |
else | |
#Delete all items if quantity is not provided | |
cart_items.with_sku(sku_id).no_bundle.delete_all | |
reset_product_credits | |
cart_items(true) | |
return update_totals | |
end | |
end | |
#Is an active card that has at least 1 order item, shipping address, payments and customer it is eligible for checkout. | |
def eligible_for_checkout? | |
(validate_active? ) ; validate_at_least_one_item?; validate_shipping_address?; validate_payment?; validate_customer? | |
return errors.empty? | |
end | |
def eligible_for_pending_removal? | |
if current_state.events.include? :pending_removal | |
return true | |
else | |
self.errors.add(:base, "Cart is not eligible for removal") | |
return false | |
end | |
end | |
def update_tax(tax_version, tax_cents, items) | |
begin | |
self.with_lock do | |
total_tax_cart_items = 0 | |
shipping_tax = 0 | |
all_ids_included = self.cart_items.pluck(:id).sort == items.map{|item| Integer(item['item_id']) }.sort | |
if tax_version == self.tax_version && all_ids_included | |
self.update_attributes!(tax_cents: tax_cents, tax_calculated: true) | |
self.set_total | |
items.each do |item| | |
total_tax_cart_items+= item['tax_cents'].to_i | |
self.cart_items.find(item['item_id']).update_attributes!(tax_cents: item['tax_cents']) | |
end | |
shipping_tax = tax_cents - total_tax_cart_items | |
Rails.logger.info_with_event("public_order_id=#{self.public_order_id} tax=#{tax_cents} total_tax_cart_items=#{total_tax_cart_items} shipping_tax=#{shipping_tax}", "update_tax", "response_received") | |
self.update_attributes!(shipping_fee_tax_cents: shipping_tax.to_i) | |
self.save! | |
return true | |
end | |
end | |
rescue => e | |
Rails.logger.info_with_event("unable to update_tax: #{e.message}", "update_tax", "error") | |
return false | |
end | |
return false | |
end | |
def update_tax_version | |
success = false | |
tax_version = self.tax_version | |
recalculate_shipping_total | |
ActiveRecord::Base.transaction do | |
self.set_total | |
self.save! # `lock!` reloads the object, so saving any pending updates is needed | |
self.lock! | |
if tax_version == self.tax_version && self.shipping_address && self.cart_items.present? | |
self.tax_version += 1 | |
self.tax_calculated = false | |
self.save! | |
success = true | |
end | |
end | |
RosiProducer.tax_queue.send_message("calculate", self.as_hash_with_associations) if success | |
rescue ActiveRecord::RecordInvalid => e | |
end | |
def as_hash_with_associations | |
attributes.merge(shipping_address: shipping_address.attributes, cart_items: cart_items.map(&:attributes)) | |
end | |
def set_total | |
self.store_credits_total = [store_credits_total, subtotal + tax + shipping_total].min | |
self.total = self.subtotal + self.tax + self.shipping_total - self.store_credits_total | |
end | |
def update_promotional_discount | |
return if cart_items.empty? | |
cart_items.update_all(promotional_discount_cents: 0) | |
PromotionService.evaluate(self).tap do |response| | |
if response.respond_to?(:promotional_shipping) | |
update_attributes(promotional_shipping: response.promotional_shipping, | |
promotional_shipping_fee: response.promotional_shipping_fee) | |
end | |
response.items.each do |item| | |
if item["promotional_discount"].present? | |
cart_item = cart_items.where(id: item["id"]).first | |
cart_item.update_promotional_discount(item["promotional_discount"]["cents"]) if cart_item | |
end | |
end | |
end | |
end | |
def accepts_cart_item_discount?(cart_item) | |
items = cart_items.reject{|item| item.id == cart_item.id} #Reject item with same id to include the last version | |
total = calculate_total(items + [cart_item]) | |
return total >= 0 | |
end | |
def update_totals(options = {}) | |
recalculate_total | |
update_promotional_discount | |
recalculate_total | |
recalculate_total_prv | |
recalculate_total_pcv | |
recalculate_pqv | |
recalculate_shipping_total | |
update_tax_version unless options[:skip_tax_recalculation] | |
save | |
end | |
def promotions_total(items=cart_items) | |
Money.new(items_cents(items,:promotional_discount_cents)) | |
end | |
def hostess_discounts_amount(items=cart_items) | |
Money.new(items_cents(items,:hostess_discount_value_cents)) | |
end | |
def hostess_discounts_total(items=cart_items) | |
items.is_a?(Array) ? items.count(&:hostess_discount?) : items.where(hostess_discount:true).count | |
end | |
def oms_serializable_hash | |
serializable_hash(include: {cart_items:{:include => {:bundle => {:only => [:id, :code, :name, :thumbnail_url, :descriptor, :product_id, :style_id]}}}, addresses: {methods: :type}, payment_token:{}, payment_auths: {methods: :payment_type}}, methods: :type_code) | |
end | |
def can_earn_pqv? | |
true | |
end | |
def can_earn_pcv? | |
true | |
end | |
def can_earn_prv? | |
true | |
end | |
def show_dsr? | |
false | |
end | |
def show_party? | |
false | |
end | |
def refresh!(force_update = false) | |
begin | |
self.with_lock do | |
cart_items.group_by(&:sku_id).each do |sku_id,items| | |
refresh_items sku_id, items, force_update | |
end | |
reset_product_credits | |
update_totals | |
end | |
rescue => e | |
Rails.logger.info_with_event("unable to refresh cart: #{e.message}", "refresh", "error") | |
end | |
end | |
private | |
def authorize_credit_card | |
if total > 0 | |
payment_token = self.payment_token.payment_token | |
auth = Auth::CreditCardAuth.create_authorization(self, payment_token) | |
log_event("successful=#{auth.successful?}", "checkout", "cc_auth_completed") | |
return auth.successful? | |
end | |
return true | |
end | |
def authorize_credits | |
# Authorize Store Credits, Hostess Credits and Hostess Discount | |
credits_auth_success = PaymentAuth.create_credit_authorizations(self) | |
log_event("successful=#{credits_auth_success}", "checkout", "credits_auth_completed") | |
return credits_auth_success | |
end | |
def need_to_update_cart_item?(item) | |
item.updated_at < 1.day.ago | |
end | |
def refresh_items(sku_id, items, force_update) | |
if sku = CatalogService.get_sku(sku_id) | |
valid_stock = validate_stock(sku.code,items.count) | |
if valid_stock | |
items.each do |item| | |
if sku && (need_to_update_cart_item?(item) || force_update) | |
item.update_sku_data(sku) | |
item.save | |
end | |
end | |
else | |
cart_items.with_sku(sku_id).destroy_all | |
end | |
end | |
end | |
def set_marketing_email_info | |
customer_receive_marketing_emails_at = customer.reload_user.receive_marketing_emails_at | |
if customer_receive_marketing_emails_at | |
self.add_to_mailing_list = true | |
self.receive_marketing_emails_at = customer_receive_marketing_emails_at | |
else | |
self.add_to_mailing_list = false | |
self.receive_marketing_emails_at = nil | |
end | |
end | |
def assign_receive_marketing_emails_at | |
self.receive_marketing_emails_at = add_to_mailing_list ? (receive_marketing_emails_at || Time.now) : nil | |
end | |
def recalculate_hostess_credits_total(items=cart_items) | |
self.hostess_credits_total = calculate_hostess_credits_total(items) | |
end | |
def recalculate_product_credits_total(items=cart_items) | |
self.product_credits_total = calculate_product_credits_total(items) | |
end | |
# Calculates shipping total | |
def calculate_shipping_total(items) | |
shipping_total = Money.new(items.map(&:shipping_surcharge).sum) | |
shipping_total += (promotional_shipping ? promotional_shipping_fee : shipping_rate_fee) if shipping_rate_fee | |
shipping_total | |
end | |
# Calculates hostess_credits_total_cents | |
def calculate_hostess_credits_total(items) | |
Money.new(items_cents(items,:hostess_credits_cents)) | |
end | |
# Calculates the total for the giving items | |
def calculate_product_credits_total(items) | |
Money.new(items_cents(items,:product_credits_cents)) | |
end | |
# Calculates item total | |
def calculate_item_total(items) | |
items.map(&:price).sum | |
end | |
# This method just calculates what the total would be for validation. | |
# Use recalculate_total if you are planning to update Cart in memory | |
# Use update_totals if you are planning to save new total in DB (so that tax gets triggered) | |
def calculate_total(items) | |
item_total = calculate_item_total(items) | |
product_credits_total = calculate_product_credits_total(items) | |
hostess_credits_total = calculate_hostess_credits_total(items) | |
shipping_total = calculate_shipping_total(items) | |
subtotal = item_total - promotions_total(items) - product_credits_total - hostess_credits_total - hostess_discounts_amount(items) | |
store_credits_total = [self.store_credits_total, subtotal + self.tax + shipping_total].min.abs | |
total = subtotal + self.tax + shipping_total - store_credits_total | |
end | |
# Updates total and all related variables in Cart | |
def recalculate_total(items = cart_items.all) | |
self.item_total = calculate_item_total(items) | |
recalculate_product_credits_total(items) | |
recalculate_hostess_credits_total(items) | |
recalculate_shipping_total(items) | |
self.subtotal = item_total - promotions_total(items) - product_credits_total - hostess_credits_total - hostess_discounts_amount(items) | |
self.set_total | |
end | |
def recalculate_pqv(items = cart_items.all) | |
self.total_pqv = items.map(&:pqv).sum | |
end | |
def recalculate_total_prv(items = cart_items.all) | |
self.total_prv = items.map(&:prv).sum | |
end | |
def recalculate_total_pcv(items = cart_items.all) | |
self.total_pcv = items.map(&:pcv).sum | |
end | |
def recalculate_shipping_total(items = cart_items.all) | |
self.shipping_total = calculate_shipping_total(items) | |
end | |
def items_cents(items,key) | |
items.is_a?(Array) ? items.sum(&key) : items.sum(key) | |
end | |
def import_items(sku_id, quantity) | |
items = [] | |
new_cart_item = new_item(sku_id) | |
items = Array.new(quantity, new_cart_item) | |
CartItem.import items | |
reset_product_credits | |
self.cart_items(true) #Reload items after importing | |
update_totals | |
end | |
def new_item(sku_id) | |
cart_item = CartItem.new(cart_id: self.id) | |
cart_item.sku_id = sku_id | |
cart_item | |
end | |
def validate_item(item) | |
unless valid = item.valid? | |
item.errors.each do |attribute, message| | |
attribute = "cart_items.#{attribute}" | |
self.errors[attribute] << message | |
self.errors[attribute].uniq! | |
end | |
end | |
return valid | |
end | |
def validate_stock(sku_code, quantity) | |
in_stock = InventoryService.in_stock?(sku_code, quantity) | |
if !in_stock | |
errors.add :quantity, "Not in stock" | |
end | |
in_stock | |
end | |
def validate_quantity(quantity) | |
if quantity <= 0 | |
errors.add :base, "quantity should be greater than zero" | |
elsif quantity >= 5000 | |
errors.add :base, "quantity should be less than 5000" | |
else | |
return true | |
end | |
return false | |
end | |
def validate_within_product_limit(item, quantity, skip_items_count=nil) | |
within = quantity + (skip_items_count ? 0 : cart_items.with_sku(item.sku_id).count) <= item.max_in_cart | |
errors.add :base, "quantity should be less than the product's maximum purchasable quantity: #{item.max_in_cart}" unless within | |
within | |
end | |
def validate_quantity_and_item(sku_id, quantity, skip_items_count=nil) | |
item = new_item(sku_id) | |
valid_item = validate_item(item) | |
return false unless valid_item | |
valid_quantity = validate_quantity(quantity) | |
within_product_limit = validate_within_product_limit(item, quantity, skip_items_count) | |
valid_quantity && within_product_limit | |
end | |
def validate_active? | |
errors.add :workflow_state, "should be active" unless self.active? | |
end | |
def validate_at_least_one_item? | |
unless cart_items.count > 0 | |
errors.add :cart_items, "at least one is required" | |
end | |
end | |
def validate_shipping_address? | |
#TODO: Implement | |
end | |
def validate_payment? | |
if !self.payment_token || !self.payment_token.payment_token | |
errors.add :payment, "payment_token not found" | |
end | |
end | |
def validate_customer? | |
unless customer_id.present? | |
errors.add :customer_id, :blank | |
end | |
end | |
def skip_customer_validation? | |
false | |
end | |
def force_customer_on_save? | |
true | |
end | |
def force_dsr_on_save? | |
true | |
end | |
def validate_promotions | |
if store_credits_total_cents_changed? && store_credits_total_cents > 0 | |
if available_balance("store_credits") < 0 | |
errors.add(:store_credits_total_cents, "Available user balance is exceeded") | |
end | |
if self.total < 0 | |
errors.add(:store_credits_total_cents, "Store credits should not exceed total price") | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment