module JamRuby

  # a sale is created every time someone tries to buy something
  class Sale < ActiveRecord::Base

    JAMTRACK_SALE = 'jamtrack'
    LESSON_SALE = 'lesson'
    POSA_SALE = 'posacard'
    SUBSCRIPTION_SALE = 'subscription'

    SOURCE_RECURLY = 'recurly'
    SOURCE_IOS = 'ios'
    SOURCE_PAYPAL = 'paypal'

    belongs_to :retailer, class_name: 'JamRuby::Retailer'
    belongs_to :user, class_name: 'JamRuby::User'
    has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem'

    has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale, foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id'

    validates :order_total, numericality: {only_integer: false}
    #validates :user
    #validates :retailer

    @@log = Logging.logger[Sale]

    def self.index(user, params = {})

      limit = params[:per_page]
      limit ||= 20
      limit = limit.to_i

      query = Sale.limit(limit)
                  .includes([:recurly_transactions, :sale_line_items])
                  .where('sales.user_id' => user.id)
                  .order('sales.created_at DESC')

      current_page = params[:page].nil? ? 1 : params[:page].to_i
      next_page = current_page + 1

      # will_paginate gem
      query = query.paginate(:page => current_page, :per_page => limit)

      if query.length == 0 # no more results
        {query: query, next_page: nil}
      elsif query.length < limit # no more results
        {query: query, next_page: nil}
      else
        {query: query, next_page: next_page}
      end

    end

    def state
      original_total = self.recurly_total_in_cents

      is_voided = false
      refund_total = 0

      recurly_transactions.each do |transaction|
        if transaction.is_voided?
          is_voided = true
        else

        end

        if transaction.is_refund?
          refund_total = refund_total + transaction.amount_in_cents
        end
      end

      # if refund_total is > 0, then you have a refund.
      # if voided is true, then in theory the whole thing has been refunded
      {
          voided: is_voided,
          original_total: original_total,
          refund_total: refund_total
      }
    end

    # The expectation is that this code would throw an exception (breaking the transaction that encompasses it),
    # if it can't validate the receipt, or communicate with Apple at all, etc
    #
    # So, if this raises exceptions, you can handle them in the stubbed out begin/rescue in ApiJamTracksController#ios_order_placed
    def self.validateIOSReceipt(receipt, price_data, user, sale)
      # these are all 'in cents' (as painfully named to be very clear), and all expected to be integers
      price = price_data['product_price'].to_f * 100.0

      price_info = {
          subtotal_in_cents: price,
          total_in_cents: price,
          tax_in_cents: nil,
          currency: price_data['product_currency']
      }
      response = IosReceiptValidator.post('/verifyReceipt',
                                          body: {'receipt-data' => receipt}.to_json,
                                          headers: {'Content-Type' => 'application/json'})
      json_resp = JSON.parse(response.body)

      #  https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1

      if 0 != json_resp['status']
        err_msgs = {
            21000 => 'The App Store could not read the JSON object you provided.',
            21002 => 'The data in the receipt-data property was malformed or missing.',
            21003 => 'The receipt could not be authenticated.',
            21005 => 'The receipt server is not currently available.',
            21007 => 'This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.',
            21008 => 'This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.'
        }
        raise err_msgs[json_resp['status']]
      else
        receiptJson = SaleReceiptIOS.new
        receiptJson.user = user
        receiptJson.sale = sale
        receiptJson.data_blob = json_resp
        receiptJson.save!
      end
      price_info
    end

    def self.ios_purchase(current_user, jam_track, receipt, price_data, variant)

      if variant.nil?
        variant = ShoppingCart::JAMTRACK_STREAM
      end

      # see if we should bail because we already own these rights
      jam_track_right = jam_track.right_for_user(current_user)

      if !jam_track_right.nil?  && jam_track_right.can_download
        # if the user already has full rights to the JamTrack, there is nothing else to do in this path
        return jam_track_right
      end

      if !jam_track_right.nil? && (!jam_track_right.can_download && variant == ShoppingCart::JAMTRACK_STREAM)
        # if the user does have the track, but isn't upgrading it, bail
        return jam_track_right
      end

      # everything needs to go into a transaction!  If anything goes wrong, we need to raise an exception to break it
      Sale.transaction do

        using_free_credit = current_user.redeem_free_credit

        sale = create_jam_track_sale(current_user, SOURCE_IOS)

        if sale.valid?

          if using_free_credit
            SaleLineItem.create_from_jam_track(current_user, sale, jam_track, using_free_credit)
            sale.recurly_subtotal_in_cents = 0
            sale.recurly_tax_in_cents = 0
            sale.recurly_total_in_cents = 0
            sale.recurly_currency = 'USD'
            sale.save!
          else

            price_info = validateIOSReceipt(receipt, price_data, current_user, sale)

            SaleLineItem.create_from_jam_track(current_user, sale, jam_track, using_free_credit)
            sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents]
            sale.recurly_tax_in_cents = price_info[:tax_in_cents]
            sale.recurly_total_in_cents = price_info[:total_in_cents]
            sale.recurly_currency = price_info[:currency]
            sale.save!
          end
        else
          # should not get out of testing.  This would be very rare (i.e., from a big regression).  Sale is always valid at this point.
          raise "invalid sale object"
        end

        # if we make it this far, all is well!
        jam_track_right = JamRuby::JamTrackRight.find_or_create_by({user_id: current_user.id, jam_track_id: jam_track.id}) do |jam_track_right|
          jam_track_right.redeemed = using_free_credit
          jam_track_right.version = jam_track.version
        end

        if variant == ShoppingCart::JAMTRACK_DOWNLOAD || variant == ShoppingCart::JAMTRACK_FULL
          jam_track_right.can_download = true
          jam_track_right.save
        end
      end

      jam_track_right
    end

    # place_order will create one or more sales based on the contents of shopping_carts for the current user
    # individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation)
    # jamtracks however can be piled onto the same sale as adjustments (VRFS-3028)
    # so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack
    def self.place_order(current_user, shopping_carts, paypal = false)

      sales = []


      #if Sale.is_mixed(shopping_carts)
      #  # the controller checks this too; this is just an extra-level of sanity checking
      #  return sales
      #end

      jam_track_sale = order_jam_tracks(current_user, shopping_carts, paypal)
      sales << jam_track_sale if jam_track_sale

      # TODO: process shopping_carts_subscriptions

      sales
    end


    def self.is_only_freebie(shopping_carts)
      free = true
      shopping_carts.each do |cart|
        free = cart.product_info[:free]

        if !free
          break
        end
      end
      free
    end

    # we don't allow mixed shopping carts :/
    def self.is_mixed(shopping_carts)
      free = false
      non_free = false
      shopping_carts.each do |cart|
        if cart.product_info[:free]
          free = true
        else
          non_free = true
        end
      end
      free && non_free
    end

    def self.purchase_test_drive(current_user, lesson_package_type, booking = nil, posa_card = nil)
      self.purchase_lesson(nil, current_user, booking, lesson_package_type, nil, nil, false, posa_card)
    end

    def self.post_sale_test_failure
      return true
    end

    def self.posa_activate(posa_card, retailer)
      sale = nil
      Sale.transaction(:requires_new => true) do

        posa_card.activate(retailer)

        if !posa_card.errors.any?

          sale = create_posa_sale(retailer, posa_card)

          SaleLineItem.create_from_posa_card(sale, retailer, posa_card)

          sale.save
        end

      end
      {sale: sale}
    end

    def self.purchase_subscription(current_user, recurly_token, plan_code)
      sale = nil

      Sale.transaction(:requires_new => true) do

        current_user.recurly_token = recurly_token
        current_user.subscription_plan_code = plan_code

        sale = create_subscription_sale(current_user)

        if sale.valid?

          client = RecurlyClient.new
          # this is handled in update_payment now
          #account = client.find_or_create_account(current_user, nil, recurly_token)
          #client.update_billing_info_from_token(current_user, account, recurly_token)

          account = client.get_account(current_user)
          if account.present?
            recurly_response = client.create_subscription(current_user, plan_code, account)
            current_user.recurly_subscription_id = recurly_response.uuid
            current_user.save(validate: false)
            SaleLineItem.create_from_subscription(current_user, sale, plan_code, recurly_response)

            sale.recurly_subtotal_in_cents = recurly_response.unit_amount_in_cents
            sale.recurly_tax_in_cents = recurly_response.tax_in_cents
            sale.recurly_total_in_cents = sale.recurly_subtotal_in_cents + sale.recurly_tax_in_cents
            sale.recurly_currency = recurly_response.currency
            sale.save(validate: false)
          else
            puts "Could not find account to place order."
            raise RecurlyClientError, "Could not find account to place order."
          end
        end
      end

      {sale: sale}
    end

    # this is easy to make generic, but right now, it just purchases lessons
    def self.purchase_lesson(charge, current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil, force = false, posa_card = nil)
      stripe_charge = nil
      sale = nil
      purchase = nil
      # everything needs to go into a transaction!  If anything goes wrong, we need to raise an exception to break it
      Sale.transaction(:requires_new => true) do

        sale = create_lesson_sale(current_user)

        if sale.valid?

          if lesson_booking
            lesson_booking.current_lesson = lesson_session
            lesson_booking.current_purchase = lesson_package_purchase
          end

          sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking)

          price_info = charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force, posa_card)

          post_sale_test_failure

          if price_info[:purchase] && price_info[:purchase].errors.any?
            purchase = price_info[:purchase]
            raise ActiveRecord::Rollback
          end

          if !sale_line_item.valid?
            raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}"
          end
          # sale.source = 'stripe'
          sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents]
          sale.recurly_tax_in_cents = price_info[:tax_in_cents]
          sale.recurly_total_in_cents = price_info[:total_in_cents]
          sale.recurly_currency = price_info[:currency]
          sale.stripe_charge_id = price_info[:charge_id]
          sale.save
          stripe_charge = price_info[:charge]
          purchase = price_info[:purchase]
        else
          # should not get out of testing.  This would be very rare (i.e., from a big regression).  Sale is always valid at this point.
          puts "invalid sale object"
          raise "invalid sale object"
        end
      end

      {sale: sale, stripe_charge: stripe_charge, purchase: purchase}
    end

    def self.charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session = nil, lesson_package_purchase = nil, force = false, posa_card = nil)
      if lesson_package_purchase
        target = lesson_package_purchase
      elsif lesson_session
        target = lesson_session
      else
        target = lesson_package_type
      end

      current_user.sync_stripe_customer

      purchase = lesson_package_purchase
      purchase = LessonPackagePurchase.create(current_user, lesson_booking, lesson_package_type, nil, nil, posa_card) if purchase.nil?

      if purchase.errors.any?
        price_info = {}
        price_info[:purchase] = purchase
        return price_info
      end

      if lesson_session
        lesson_session.lesson_package_purchase_id = purchase.id
        lesson_session.save!
      end

      subtotal_in_cents = purchase.price_in_cents

      tax_percent = 0
      if current_user.stripe_zip_code
        lookup = ZipCodes.identify(current_user.stripe_zip_code)
        if lookup && lookup[:state_code] == 'TX'
          tax_percent = 0.0825
        end
      end

      tax_in_cents = (subtotal_in_cents * tax_percent).round
      total_in_cents = subtotal_in_cents + tax_in_cents

      if lesson_session # not set if test drive
        lesson_id = lesson_session.id
        teacher_id = lesson_session.teacher.id
        teacher_name = lesson_session.teacher.name
      end

      charge_id = charge.id if charge # not set if test drive

      begin
        metadata = {
            lesson_package: purchase.id,
            lesson_session: lesson_id,
            teacher_id: teacher_id,
            teacher_name: teacher_name,
            charge: charge_id,
            user: current_user.id,
            tax: tax_in_cents
        }
      rescue Exception => e
        metadata = {metaerror: true}
      end

      stripe_charge = Stripe::Charge.create(
          :amount => total_in_cents,
          :currency => "usd",
          :customer => current_user.stripe_customer_id,
          :description => target.stripe_description(lesson_booking),
          :metadata => metadata
      )
      if charge
        charge.stripe_charge = stripe_charge
      end

      sale_line_item.lesson_package_purchase = purchase
      sale_line_item.save

      price_info = {}
      price_info[:subtotal_in_cents] = subtotal_in_cents
      price_info[:tax_in_cents] = tax_in_cents
      price_info[:total_in_cents] = total_in_cents
      price_info[:currency] = 'USD'
      price_info[:charge_id] = stripe_charge.id
      price_info[:charge] = stripe_charge
      price_info[:purchase] = purchase
      price_info
    end


    # this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed)
    # it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned
    def self.order_jam_tracks(current_user, shopping_carts, is_paypal)

      shopping_carts_jam_tracks = []
      shopping_carts_subscriptions = []
      shopping_carts_gift_cards = []

      shopping_carts.each do |shopping_cart|
        if shopping_cart.is_jam_track?
          shopping_carts_jam_tracks << shopping_cart
        elsif shopping_cart.is_gift_card?
          shopping_carts_gift_cards << shopping_cart
        else
          # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
          raise "unknown shopping cart type #{shopping_cart.cart_type}"
          shopping_carts_subscriptions << shopping_cart
        end
      end

      sale = nil
      Sale.transaction do
        sale = create_jam_track_sale(current_user, is_paypal ? SOURCE_PAYPAL : SOURCE_RECURLY)

        if sale.valid?
          if is_only_freebie(shopping_carts)
            sale.process_shopping_carts(current_user, shopping_carts, nil)

            sale.recurly_subtotal_in_cents = 0
            sale.recurly_tax_in_cents = 0
            sale.recurly_total_in_cents = 0
            sale.recurly_currency = 'USD'

            if sale.sale_line_items.count == 0
              @@log.info("no sale line items associated with sale")
              # we must have ditched some of the sale items.   let's just abort this sale
              sale.destroy
              sale = nil
              return sale
            end

            sale.sale_line_items.each do |sale_line_item|
              sale_line_item = sale.sale_line_items[0]
              sale_line_item.recurly_tax_in_cents = 0
              sale_line_item.recurly_total_in_cents = 0
              sale_line_item.recurly_currency = 'USD'
              sale_line_item.recurly_discount_in_cents = 0
            end
            sale.save

          else

            if is_paypal

              sale.process_shopping_carts(current_user, shopping_carts)

              paypal_auth = current_user.paypal_auth

              @api = PayPal::SDK::Merchant::API.new
              @get_express_checkout_details = @api.build_get_express_checkout_details({:Token => paypal_auth.token})
              @response = @api.get_express_checkout_details(@get_express_checkout_details)

              @@log.info("User #{current_user.email}, GetExpressCheckout: #{@response.inspect}")

              tax = false
              if @response.Ack == 'Success'
                payerInfo = @response.GetExpressCheckoutDetailsResponseDetails.PayerInfo
                if payerInfo.Address && ( payerInfo.Address.Country == 'US' && payerInfo.Address.StateOrProvince == 'TX')
                  # we need to ask for taxes
                  tax = true
                end
              end

              tax_rate = tax ? 0.0825 : 0
              total = current_user.shopping_cart_total.round(2)
              tax_total = (total * tax_rate).round(2)
              total = total + tax_total
              total = total.round(2)

              @do_express_checkout_payment = @api.build_do_express_checkout_payment({
                                                                                        :DoExpressCheckoutPaymentRequestDetails => {
                                                                                            :PaymentDetails =>
                                                                                                [
                                                                                                    {
                                                                                                        :OrderTotal => {
                                                                                                            :currencyID => "USD",
                                                                                                            :value => total
                                                                                                        },
                                                                                                        :PaymentAction => "Sale"
                                                                                                    }
                                                                                                ],
                                                                                            :Token => paypal_auth.token,
                                                                                            :PayerID => paypal_auth.uid, }})
              @pay_response = @api.do_express_checkout_payment(@do_express_checkout_payment)

              @@log.info("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}")

              # #<PayPal::SDK::Merchant::DataTypes::DoExpressCheckoutPaymentResponseType:0x007fe511dd9b88
              # @Timestamp=Sun, 11 Dec 2016 02:09:31 +0000, @Ack="Success",
              # @CorrelationID="b28faf6bd90d9", @Version="117.0", @Build="000000",
              # @DoExpressCheckoutPaymentResponseDetails=#<PayPal::SDK::Merchant::DataTypes::DoExpressCheckoutPaymentResponseDetailsType:0x007fe511dd38c8
              # @Token="EC-7A4606566T700564B",
              # @PaymentInfo=[#<PayPal::SDK::Merchant::DataTypes::PaymentInfoType:0x007fe511dd3008 @TransactionID="63C410710F2619403", @ParentTransactionID=nil,
              # @ReceiptID=nil, @TransactionType="express-checkout", @PaymentType="instant", @PaymentDate=Sun, 11 Dec 2016 02:09:31 +0000,
              # @GrossAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dd0c90 @currencyID="USD", @value="1.99">,
              # @FeeAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dd04e8 @currencyID="USD", @value="0.36">,
              # @TaxAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dcbe98 @currencyID="USD", @value="0.00">,
              # @ExchangeRate=nil, @PaymentStatus="Completed", @PendingReason="none", @ReasonCode="none", @ProtectionEligibility="Eligible",
              # @ProtectionEligibilityType="ItemNotReceivedEligible,UnauthorizedPaymentEligible", @SellerDetails=#<PayPal::SDK::Merchant::DataTypes::SellerDetailsType:0x007fe511dcb358 @SecureMerchantAccountID="6MB486RSBRMJ2">>],
              # @SuccessPageRedirectRequested="false", @CoupledPaymentInfo=[#<PayPal::SDK::Merchant::DataTypes::CoupledPaymentInfoType:0x007fe511dca7a0>]>>

              if @pay_response.Ack == 'Success'
                details = @pay_response.DoExpressCheckoutPaymentResponseDetails.PaymentInfo[0]
                sale.recurly_invoice_id = details.TransactionID
                sale.recurly_invoice_number = details.ReceiptID

                # now slap in all the real tax/purchase totals
                sale.recurly_subtotal_in_cents = ((details.GrossAmount.value.to_f - details.TaxAmount.value.to_f) * 100).to_i
                sale.recurly_tax_in_cents = (details.TaxAmount.value.to_f * 100).to_i
                sale.recurly_total_in_cents = (details.GrossAmount.value.to_f * 100).to_i
                sale.recurly_currency = details.GrossAmount.currencyID

                unless sale.save
                  puts "Invalid sale (at end)."
                  raise PayPalClientError, "Invalid sale (at end)."
                end
              else
                @@log.error("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}")
                raise PayPalClientError, @pay_response.Errors[0].LongMessage
              end
            else
              client = RecurlyClient.new
              account = client.get_account(current_user)
              if account.present?

                purge_pending_adjustments(account)

                created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account)

                # now invoice the sale ... almost done

                begin
                  invoice = account.invoice!
                  sale.recurly_invoice_id = invoice.uuid
                  sale.recurly_invoice_number = invoice.invoice_number

                  # now slap in all the real tax/purchase totals
                  sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents
                  sale.recurly_tax_in_cents = invoice.tax_in_cents
                  sale.recurly_total_in_cents = invoice.total_in_cents
                  sale.recurly_currency = invoice.currency

                  # and resolve against sale_line_items
                  sale.sale_line_items.each do |sale_line_item|
                    found_line_item = false
                    invoice.line_items.each do |line_item|
                      if line_item.uuid == sale_line_item.recurly_adjustment_uuid
                        sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents
                        sale_line_item.recurly_total_in_cents =line_item.total_in_cents
                        sale_line_item.recurly_currency = line_item.currency
                        sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents
                        found_line_item = true
                        break
                      end

                    end

                    if !found_line_item
                      @@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
                      puts "CANT FIND LINE ITEM"
                    end
                  end

                  unless sale.save
                    puts "Invalid sale (at end)."
                    raise RecurlyClientError, "Invalid sale (at end)."
                  end
                rescue Recurly::Resource::Invalid => e
                  # this exception is thrown by invoice! if the invoice is invalid
                  sale.rollback_adjustments(current_user, created_adjustments)
                  sale = nil
                  raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
                rescue => e
                  puts "UNKNOWN E #{e}"
                end
              else
                raise RecurlyClientError, "Could not find account to place order."
              end
            end
          end
        else
          raise RecurlyClientError, "Invalid sale."
        end
      end
      sale
    end

    def process_shopping_carts(current_user, shopping_carts, account = nil)

      created_adjustments = []

      begin
        shopping_carts.each do |shopping_cart|
          process_shopping_cart(current_user, shopping_cart, account, created_adjustments)
        end
      rescue Recurly::Error, NoMethodError => x
        # rollback any adjustments created if error
        rollback_adjustments(user, created_adjustments)
        raise RecurlyClientError, x.to_s
      rescue Exception => e
        # rollback any adjustments created if error
        rollback_adjustments(user, created_adjustments)
        raise e
      end

      created_adjustments
    end


    def process_shopping_cart(current_user, shopping_cart, recurly_account, created_adjustments)
      recurly_adjustment_uuid = nil
      recurly_adjustment_credit_uuid = nil

      # we do this because of ShoppingCart.remove_jam_track_from_cart; if it occurs, which should be rare, we need fresh shopping cart info
      shopping_cart.reload

      # get the JamTrack in this shopping cart
      cart_product = shopping_cart.cart_product

      if shopping_cart.is_jam_track?
        jam_track = cart_product

        if jam_track.right_for_user(current_user, shopping_cart.variant)
          # if the user already owns the JamTrack, we should just skip this cart item, and destroy it
          # if this occurs, we have to reload every shopping_cart as we iterate.  so, we do at the top of the loop
          ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
          return
        end
      end


      if recurly_account
        # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
        adjustments = shopping_cart.create_adjustment_attributes(current_user)

        adjustments.each do |adjustment|

          # create the adjustment at Recurly (this may not look like it, but it is a REST API)
          created_adjustment = recurly_account.adjustments.new(adjustment)
          created_adjustment.save

          # if the adjustment could not be made, bail
          raise RecurlyClientError.new(created_adjustment.errors) if created_adjustment.errors.any?

          # keep track of adjustments we created for this order, in case we have to roll them back
          created_adjustments << created_adjustment

          if ShoppingCart.is_product_purchase?(adjustment)
            # this was a normal product adjustment, so track it as such
            recurly_adjustment_uuid = created_adjustment.uuid
          else
            # this was a 'credit' adjustment, so track it as such
            recurly_adjustment_credit_uuid = created_adjustment.uuid
          end
        end
      end


      # create one sale line item for every jam track
      sale_line_item = SaleLineItem.create_from_shopping_cart(self, shopping_cart, nil, recurly_adjustment_uuid, recurly_adjustment_credit_uuid)

      # if the sale line item is invalid, blow up the transaction
      unless sale_line_item.valid?
        @@log.error("sale item invalid! #{sale_line_item.errors.inspect}")
        puts("sale item invalid! #{sale_line_item.errors.inspect}")
        Stats.write('web.recurly.purchase.sale_invalid', {message: sale_line_item.errors.to_s, value: 1})
        raise RecurlyClientError.new(sale_line_item.errors)
      end

      if shopping_cart.is_jam_track?
        jam_track = cart_product

        # create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident)
        jam_track_right = JamRuby::JamTrackRight.find_or_create_by({user_id: current_user.id, jam_track_id: jam_track.id}) do |jam_track_right|
          jam_track_right.redeemed = shopping_cart.free?
          jam_track_right.version = jam_track.version
        end


        # deal with variant behavior
        if shopping_cart.purchasing_downloadable_rights?
          jam_track_right.can_download = true
          jam_track_right.save
        end

        # also if the purchase was a free one, then:
        # first, mark the free has_redeemable_jamtrack field if that's still true
        # and if still they have more free things, then redeem the giftable_jamtracks
        if shopping_cart.free?
          current_user.redeem_free_credit
        end

        # this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
        if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid
          jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid
          jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
          unless jam_track_right.save
            raise RecurlyClientError.new(jam_track_right.errors)
          end
        end

        # blow up the transaction if the JamTrackRight did not get created
        raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?

      elsif shopping_cart.is_gift_card?
        gift_card_type = cart_product
        raise "gift card is null" if gift_card_type.nil?
        raise if current_user.nil?

        shopping_cart.quantity.times do |item|
          gift_card_purchase = GiftCardPurchase.new(
              {
                  user: current_user,
                  gift_card_type: gift_card_type
              })

          unless gift_card_purchase.save
            raise RecurlyClientError.new(gift_card_purchase.errors)
          end
        end

      else
        raise 'unknown shopping cart type: ' + shopping_cart.cart_type
      end

      # delete the shopping cart; it's been dealt with
      shopping_cart.destroy if shopping_cart
    end


    def rollback_adjustments(current_user, adjustments)
      begin
        adjustments.each { |adjustment| adjustment.destroy }
      rescue Exception => e
        AdminMailer.alerts({
                               subject: "ACTION REQUIRED: #{current_user.email} did not have all of his adjustments destroyed in rollback",
                               body: "go delete any adjustments on the account that don't belong. error: #{e}\n\nAdjustments: #{adjustments.inspect}"
                           }).deliver_now

      end
    end

    def self.purge_pending_adjustments(account)
      account.adjustments.pending.find_each do |adjustment|
        # we only pre-emptively destroy pending adjustments if they appear to be created by the server
        adjustment.destroy if ShoppingCart.is_server_pending_adjustment?(adjustment)
      end
    end

    def is_jam_track_sale?
      sale_type == JAMTRACK_SALE
    end

    def is_lesson_sale?
      sale_type == LESSON_SALE
    end

    def self.create_jam_track_sale(user, sale_source=nil)
      sale = Sale.new
      sale.user = user
      sale.sale_type = JAMTRACK_SALE # gift cards and jam tracks are sold with this type of sale
      sale.order_total = 0
      sale.source = sale_source if sale_source
      sale.save
      sale
    end

    def self.create_lesson_sale(user)
      sale = Sale.new
      sale.user = user
      sale.sale_type = LESSON_SALE
      sale.order_total = 0
      sale.save
      sale
    end

    def self.create_subscription_sale(user)
      sale = Sale.new
      sale.user = user
      sale.sale_type = SUBSCRIPTION_SALE
      sale.order_total = 0
      sale.save
      sale
    end

    def self.create_posa_sale(retailer, posa_card)
      sale = Sale.new
      sale.retailer = retailer
      sale.sale_type = POSA_SALE
      sale.order_total = posa_card.product_info[:price]
      sale.save
      sale
    end

    # this checks just jamtrack sales appropriately
    def self.check_integrity_of_jam_track_sales
      Sale.select([:total, :voided]).find_by_sql(
          "SELECT COUNT(sales.id) AS total,
          COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::VOID}' THEN 1 ELSE null END) voided
          FROM sales
          LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON invoice_id = sales.recurly_invoice_id
          WHERE sale_type = '#{JAMTRACK_SALE}'")
    end
  end
end
