module JamRuby class ShoppingCart < ActiveRecord::Base # just a normal purchase; used on the description field of a recurly adjustment PURCHASE_NORMAL = 'purchase-normal' # a free purchase; used on the description field of a recurly adjustment PURCHASE_FREE = 'purchase-free' # a techinicality of Recurly; we create a free-credit adjustment to balance out the free purchase adjustment PURCHASE_FREE_CREDIT = 'purchase-free-credit' PURCHASE_REASONS = [PURCHASE_NORMAL, PURCHASE_FREE, PURCHASE_FREE_CREDIT] JAMTRACK_FULL = 'full' JAMTRACK_STREAM = 'stream' JAMTRACK_DOWNLOAD = 'download' JAMTRACK_VARIANTS = ['full', 'stream', 'download'] attr_accessible :quantity, :cart_type, :product_info attr_accessor :skip_mix_check validates_uniqueness_of :cart_id, scope: [:cart_type, :user_id, :anonymous_user_id] belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id" validates :cart_id, presence: true validates :cart_type, presence: true validates :cart_class_name, presence: true validates :marked_for_redeem, numericality: {only_integer: true} validates :variant, inclusion: {in: [nil, JAMTRACK_FULL, JAMTRACK_STREAM, JAMTRACK_DOWNLOAD]} #validate :not_mixed default_scope { order('created_at DESC') } def product_info(instance = nil) product = self.cart_product data = {type: cart_type, name: product.name, price: product.variant_price(variant), product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display(variant), allow_free: allow_free(product)} unless product.nil? if data && instance data.merge!(instance.product_info) end data end # multiply quantity by price def total_price(product) quantity * product.variant_price(variant) end def purchasing_downloadable_rights? is_jam_track? && (variant == ShoppingCart::JAMTRACK_DOWNLOAD || variant == ShoppingCart::JAMTRACK_FULL) end # multiply (quantity - redeemable) by price def real_price(product) (quantity - marked_for_redeem) * product.variant_price(variant) end def allow_free(product) if(product.is_a?(JamTrack)) product.allow_free else false end end def not_mixed return if @skip_mix_check existing_carts = [] this_user = any_user() if this_user existing_carts = this_user.shopping_carts end existing_carts = existing_carts.to_a existing_carts << self if Sale.is_mixed(existing_carts) if free? errors.add(:base, "You can not add a free JamTrack to a cart with non-free items. Please clear out your cart.") return false else errors.add(:base, "You can not add a non-free JamTrack to a cart containing free items. Please clear out your cart.") return false end end false end def cart_product self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank? end def redeem(mark_redeem) self.marked_for_redeem = mark_redeem ? 1 : 0 end def free? marked_for_redeem == quantity end def any_user if user user elsif anonymous_user_id AnonymousUser.new(anonymous_user_id, nil) else nil end end def self.create(user, product, quantity = 1, mark_redeem = false, variant = nil) cart = ShoppingCart.new if user.is_a?(User) cart.user = user else cart.anonymous_user_id = user.id end cart.cart_type = product.class::PRODUCT_TYPE if cart.cart_type == JamTrack::PRODUCT_TYPE && variant.nil? cart.variant = JAMTRACK_STREAM # default to jamtrack 'stream' else cart.variant = variant end cart.cart_class_name = product.class.name cart.cart_id = product.id cart.quantity = quantity cart.redeem(mark_redeem) cart.save cart end def is_jam_track? cart_type == JamTrack::PRODUCT_TYPE end def is_gift_card? cart_type == GiftCardType::PRODUCT_TYPE end def is_lesson? cart_type == LessonPackageType::PRODUCT_TYPE end # returns an array of adjustments for the shopping cart def create_adjustment_attributes(current_user) raise "not a jam track or gift card" unless is_jam_track? || is_gift_card? info = self.product_info if free? # create the credit, then the pseudo charge [] else [ { accounting_code: PURCHASE_NORMAL, currency: 'USD', unit_amount_in_cents: (info[:total_price] * 100).to_i, description: info[:sale_display], tax_exempt: false } ] end end def self.move_to_user(user, anonymous_user, shopping_carts) shopping_carts.each do |shopping_cart| if shopping_cart.is_jam_track? mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user) cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem, shopping_cart.variant) else cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, false, shopping_cart.variant) end end anonymous_user.destroy_all_shopping_carts end def self.is_product_purchase?(adjustment) (adjustment[:accounting_code].include?(PURCHASE_FREE) || adjustment[:accounting_code].include?(PURCHASE_NORMAL)) && !adjustment[:accounting_code].include?(PURCHASE_FREE_CREDIT) end # recurly_adjustment is a Recurly::Adjustment (http://www.rubydoc.info/gems/recurly/Recurly/Adjustment) # this asks, 'is this a pending adjustment?' AND 'was this adjustment created by the server (vs manually by someone -- we should leave those alone).' def self.is_server_pending_adjustment?(recurly_adjustment) recurly_adjustment.state == 'pending' && (recurly_adjustment.accounting_code.include?(PURCHASE_FREE) || recurly_adjustment.accounting_code.include?(PURCHASE_NORMAL) || recurly_adjustment.accounting_code.include?(PURCHASE_FREE_CREDIT)) end # if the user has a redeemable jam_track still on their account, then also check if any shopping carts have already been marked. # if no shpping carts have been marked, then mark it redeemable # should be wrapped in a TRANSACTION def self.user_has_redeemable_jam_track?(any_user) if any_user.has_redeemable_jamtrack || any_user.gifted_jamtracks > 0 free_in_cart = 0 any_user.shopping_carts.each do |shopping_cart| # but if we find any shopping cart item already marked for redeem, then back out of mark_redeem=true if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE free_in_cart += shopping_cart.marked_for_redeem end end any_user.free_jamtracks > free_in_cart else false end end # adds a jam_track to cart, checking for promotions def self.add_jam_track_to_cart(any_user, jam_track, variant = JAMTRACK_FULL) cart = nil if variant.nil? variant = JAMTRACK_FULL end ShoppingCart.transaction do # if clear if any_user.shopping_carts.length == 1 && any_user.shopping_carts[0].product_info[:allow_free] && (any_user.has_redeemable_jamtrack && any_user.gifted_jamtracks == 0) && jam_track.allow_free && any_user.free_jamtracks > 0 # clear # if you are an anonymous user, we make sure there is nothing else in your shopping cart ... keep it clean for the 'new user rummaging around for a freebie scenario' any_user.destroy_jam_track_shopping_carts any_user.reload end mark_redeem = jam_track.allow_free ? ShoppingCart.user_has_redeemable_jam_track?(any_user) : false cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem, variant) end any_user.reload cart end def self.add_item_to_cart(any_user, item) cart = nil ShoppingCart.transaction do cart = ShoppingCart.create(any_user, item, 1, false) end cart end # deletes a jam track from the shopping cart, updating redeem flag as necessary def self.remove_jam_track_from_cart(any_user, cart) ShoppingCart.transaction do cart.destroy # so that user.shopping_carts reflects truth any_user.reload # check if we should move the redemption around automatically mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) carts = any_user.shopping_carts # if we find any carts on the account that are not redeemed, mark first one redeemable if mark_redeem && carts.length > 0 carts.each do |cart| if cart.marked_for_redeem == 0 if cart.quantity > 1 raise 'unknown situation for redeemption juggling' end cart.redeem(mark_redeem) cart.save break end end end end end def self.remove_item_from_cart(any_user, cart) ShoppingCart.transaction do cart.destroy end end # if the number of items in the shopping cart is less than gifted_jamtracks on the user, then fix them all up def self.apply_gifted_jamtracks(user) jam_track_carts = user.shopping_carts.where(cart_type:JamTrack::PRODUCT_TYPE) if jam_track_carts.count > user.gifted_jamtracks # just whack everything in their shopping cart user.destroy_all_shopping_carts return end jam_track_carts.each do |cart| cart.skip_mix_check = true cart.marked_for_redeem = 1 cart.save! end end def port(user, anonymous_user) ShoppingCart.transaction do move_to_user(user, anonymous_user, anonymous_user.shopping_carts) end end end end