solidusのproductがカートに入るまでの流れをまとめてみた


solidusのカートに入るまでの流れ

solidusのfrontend gemを参考にしてその流れをコードを追って調べてみました。

①productページ

まずこちらがsolidus frontendのproducts/showのページ

こちらのページの add to cart付近のコードをみてみるとformタグでvariant_id, quantityをorders/populateにpostメソッドで送信ていた。

②orders controller

・(a)populateでは、まず@orderに現在のorderを代入している。
・次にvariant, quantityにフォームで入力した値を代入する。
・数量が1から2147483647までの間でなければ問題が出るため、極端に値が大き場合はエラーが発生するようにしている。
・(b)先ほど定義したvariantとquantityを使ってlineItemに数量追加または新規作成し、適用条件を満たしたpromotionやshipment情報があれば追加され、orderが更新される。
・上でエラーが発生すれば、エラー情報を含めてリダイレクトされる。エラーがなければ、cart_pathにレダイレクトされる。

以上がカートに商品を追加した時の流れです。多くのmodelが絡み合っていて情報を整理したと思い記述しました!
下にさらに詳しい情報を乗せています。

※(x)で以下に,コードをさらに追った時の補足情報をつけています。

orders_contoller.rb(frontend)
def populate
      # (a)
      @order = current_order(create_order_if_necessary: true)
      authorize! :update, @order, cookies.signed[:guest_token]

      variant  = Spree::Variant.find(params[:variant_id])
      quantity = params[:quantity].present? ? params[:quantity].to_i : 1

      # 2,147,483,647 is crazy. See issue https://github.com/spree/spree/issues/2695.
      if !quantity.between?(1, 2_147_483_647)
        @order.errors.add(:base, t('spree.please_enter_reasonable_quantity'))
      end

      # (b)
      begin
        @line_item = @order.contents.add(variant, quantity)
      rescue ActiveRecord::RecordInvalid => e
        @order.errors.add(:base, e.record.errors.full_messages.join(", "))
      end

      respond_with(@order) do |format|
        format.html do
          if @order.errors.any?
            flash[:error] = @order.errors.full_messages.join(", ")
            redirect_back_or_default(spree.root_path)
            return
          else
            redirect_to cart_path
          end
        end
      end
    end

(a)current_orderがわからなかったので、コードを追ってみると、以下の手順でorderを作成または取得していた。

①未完了のorderを探す。
find_order_by_token_or_user()で、orderのstatusがコンプリートしていないorderを探す。geust_tokenから探した後に、ユーザーログインしていたらそのユーザーの最後のコンプリートしていないorderを探す。

②新規のorderを作成(①でorderが見つからなければの場合)
options[:create_order_if_necessary]がtrueかつ@current_orderがnilのため、orderを新規作成し、current_userを作成したorderに代入

@current_orderにipアドレスを代入し、@current_orderを返す。
@current_order.record_ip_address(ip_address)で現在のipアドレスを代入していた。仮にすでにipアドレスが存在している場合は、すでに保存されたipアドレスと現在のipアドレスが異なる場合に更新するようになっている。

spree/core/controller_helpers_order.rb(core)
def current_order(options = {})
          options[:create_order_if_necessary] ||= false

          return @current_order if @current_order

          @current_order = find_order_by_token_or_user(options)

          if options[:create_order_if_necessary] && (@current_order.nil? || @current_order.completed?)
            @current_order = Spree::Order.new(new_order_params)
            @current_order.user ||= try_spree_current_user
            # See issue https://github.com/spree/spree/issues/3346 for reasons why this line is here
            @current_order.created_by ||= try_spree_current_user
            @current_order.save!
          end

          if @current_order
            @current_order.record_ip_address(ip_address)
            return @current_order
          end
        end

(b)@order.contents.add()がについてコードを追ってみた

このメソッドにより、LineItemの作成または既存していれば数量の追加やshipmentの追加や適用されたがあればpromotionの追加、最後に追加した情報をorderに更新していました。詳しい内容は以下の通りです。

①orderクラスのcontentsメソッドで、Spree::OrderContents.new(self)が作成され、@cotentsに代入させる。

②その@contents(Spree::OrderContentsクラスのインスタンス)に対してaddメソッドを実行している。

Order.rb(core)
    def contents
      @contents ||= Spree::OrderContents.new(self)
    end

③addメソッドをみてみると、add_to_line_itemが実行されている。add_to_line_itemでは、
まず、grab_line_item_by_variantによって、今回登録したいvariantがすでにLinItemとしてorderに登録されている場合は、そのLineItemを取り出してくる。初めて登録するvariantならnilが返る。

④続いて、上記でnilになれば、新しいLineItemを作成し、variantを追加する。
line_item.quantity += quantity.to_iにより、数量を追加。ここで既存のLineItemを使用する場合は、既存の数量に対して加算するようにしている。

⑤最後にオプションのshippmentがある場合はlineItemに追加し、保存して、LineItemを返している。

⑥返されたLineItemは、addメソッドのlien_itemに代入され、after_add_or_removeが実行される。

⑦reload_totalsメソッドでは、orderに対して現在、選択されている商品の支払い金額などを更新させ、shipmentオプションがあれば追加し、なければnilとなる。続いて、promotionが存在して、適用の条件を満たしていれば、ここで適用される。
再度reload_totalsメソッドが実行され、promotionやshipment適用後のorder更新が行われる。
最後にlineItemが返される

OrderContents.rb(core)
    def add(variant, quantity = 1, options = {})
      line_item = add_to_line_item(variant, quantity, options)
      after_add_or_remove(line_item, options)
    end

    # (省略)

    def add_to_line_item(variant, quantity, options = {})
      line_item = grab_line_item_by_variant(variant, false, options)

      line_item ||= order.line_items.new(
        quantity: 0,
        variant: variant,
      )

      line_item.quantity += quantity.to_i
      line_item.options = ActionController::Parameters.new(options).permit(PermittedAttributes.line_item_attributes).to_h

      line_item.target_shipment = options[:shipment]
      line_item.save!
      line_item
    end

      # (省略)
    def after_add_or_remove(line_item, options = {})
      reload_totals
      shipment = options[:shipment]
      shipment.present? ? shipment.update_amounts : order.ensure_updated_shipments
      PromotionHandler::Cart.new(order, line_item).activate
      reload_totals
      line_item
    end

参考

公式ドキュメントやソースコードなど