Skip to content

Instantly share code, notes, and snippets.

@zaeem
Created April 15, 2015 14:56
Show Gist options
  • Save zaeem/10da5c8ca8f8ba836e8f to your computer and use it in GitHub Desktop.
Save zaeem/10da5c8ca8f8ba836e8f to your computer and use it in GitHub Desktop.
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