Search documentation of Phoenix

# 5. Bringing It Home

View Source

With our `Catalog` and `ShoppingCart` contexts, we're seeing first-hand how our well-considered modules and function names are yielding clear and maintainable code. Our last order of business is to allow the user to initiate the checkout process. We won't go as far as integrating payment processing or order fulfillment, but we'll get you started in that direction. This will be a great opportunity to put what we have learned so far in practice.

Like before, we need to decide where code for completing an order should live. Is it part of the catalog? Clearly not, but what about the shopping cart? Shopping carts are related to orders – after all, the user has to add items in order to purchase any products – but should the order checkout process be grouped here?

If we stop and consider the order process, we'll see that orders involve related, but distinctly different data from the cart contents. Also, business rules around the checkout process are much different than carting. For example, we may allow a user to add a back-ordered item to their cart, but we could not allow an order with no inventory to be completed. Additionally, we need to capture point-in-time product information when an order is completed, such as the price of the items at payment transaction time. This is essential because a product price may change in the future, but the line items in our order must always record and display what we charged at time of purchase. For these reasons, we can start to see that ordering can stand on its own with its own data concerns and business rules.

Naming wise, `Orders` clearly defines our context, so let's get started by again taking advantage of the context generators. Note that the `user` scope generated by `mix phx.gen.auth` is marked as default scope (in your `config/config.exs`), therefore we don't need to specify it in our command. There can be different scopes in an application, in which case the `--scope` option can be used when running the generators. Run the following command in your console:

    $ mix phx.gen.context Orders Order orders total_price:decimal

    * creating lib/hello/orders/order.ex
    * creating priv/repo/migrations/20250209214612_create_orders.exs
    * creating lib/hello/orders.ex
    * injecting lib/hello/orders.ex
    * creating test/hello/orders_test.exs
    * injecting test/hello/orders_test.exs
    * creating test/support/fixtures/orders_fixtures.ex
    * injecting test/support/fixtures/orders_fixtures.ex

    Remember to update your repository by running migrations:

        $ mix ecto.migrate

We generated an `Orders` context. The order is automatically scoped to the current user and added a `total_price` column. With our starting point in place, let's open up the newly created migration in `priv/repo/migrations/*_create_orders.exs` and make the following changes:

      def change do
        create table(:orders) do
    -     add :total_price, :decimal
    +     add :total_price, :decimal, precision: 15, scale: 6, null: false
          add :user_id, references(:users, type: :id, on_delete: :delete_all)

          timestamps(type: :utc_datetime)
        end
      end

Like we did previously, we gave appropriate precision and scale options for our decimal column which will allow us to store currency without precision loss. We also added a not-null constraint to enforce all orders to have a price.

The orders table alone doesn't hold much information, but we know we'll need to store point-in-time product price information of all the items in the order. For that, we'll add an additional struct for this context named `LineItem`. Line items will capture the price of the product at payment transaction time. Please run the following command:

    $ mix phx.gen.context Orders LineItem order_line_items \
    price:decimal quantity:integer \
    order_id:references:orders product_id:references:products --no-scope

    You are generating into an existing context.
    ...
    Would you like to proceed? [Yn] y
    * creating lib/hello/orders/line_item.ex
    * creating priv/repo/migrations/20250209215050_create_order_line_items.exs
    * injecting lib/hello/orders.ex
    * injecting test/hello/orders_test.exs
    * injecting test/support/fixtures/orders_fixtures.ex

    Remember to update your repository by running migrations:

        $ mix ecto.migrate

We used the `phx.gen.context` command to generate the `LineItem` Ecto schema and inject supporting functions into our orders context. Like before, let's modify the migration in `priv/repo/migrations/*_create_order_line_items.exs` and make the following decimal field changes:

      def change do
        create table(:order_line_items) do
    -     add :price, :decimal
    +     add :price, :decimal, precision: 15, scale: 6, null: false
          add :quantity, :integer
          add :order_id, references(:orders, on_delete: :nothing)
          add :product_id, references(:products, on_delete: :nothing)

          timestamps(type: :utc_datetime)
        end

        create index(:order_line_items, [:order_id])
        create index(:order_line_items, [:product_id])
      end

With our migration in place, let's wire up our orders and line items associations in `lib/hello/orders/order.ex`:

      schema "orders" do
        field :total_price, :decimal
    -   field :user_id, :id

    +   belongs_to :user, Hello.Accounts.User
    +   has_many :line_items, Hello.Orders.LineItem
    +   has_many :products, through: [:line_items, :product]

        timestamps(type: :utc_datetime)
      end

We used `has_many :line_items` to associate orders and line items, just like we've seen before. Next, we used the `:through` feature of `has_many`, which allows us to instruct ecto how to associate resources across another relationship. In this case, we can associate products of an order by finding all products through associated line items. Next, let's wire up the association in the other direction in `lib/hello/orders/line_item.ex`:

      schema "order_line_items" do
        field :price, :decimal
        field :quantity, :integer
    -   field :order_id, :id
    -   field :product_id, :id

    +   belongs_to :order, Hello.Orders.Order
    +   belongs_to :product, Hello.Catalog.Product

        timestamps(type: :utc_datetime)
      end

We used `belongs_to` to associate line items to orders and products. With our associations in place, we can start integrating the web interface into our order process. Open up your router `lib/hello_web/router.ex` and add the following line:

      scope "/", HelloWeb do
        pipe_through [:browser, :require_authenticated_user]

        resources "/cart_items", CartItemController, only: [:create, :delete]

        get "/cart", CartController, :show
        put "/cart", CartController, :update

    +   resources "/orders", OrderController, only: [:create, :show]
      end

We wired up `create` and `show` routes for our generated `OrderController`, since these are the only actions we need at the moment. With our routes in place, we can now migrate up:

    $ mix ecto.migrate

    17:14:37.715 [info] == Running 20250209214612 Hello.Repo.Migrations.CreateOrders.change/0 forward

    17:14:37.720 [info] create table orders

    17:14:37.755 [info] == Migrated 20250209214612 in 0.0s

    17:14:37.784 [info] == Running 20250209215050 Hello.Repo.Migrations.CreateOrderLineItems.change/0 forward

    17:14:37.785 [info] create table order_line_items

    17:14:37.795 [info] create index order_line_items_order_id_index

    17:14:37.796 [info] create index order_line_items_product_id_index

    17:14:37.798 [info] == Migrated 20250209215050 in 0.0s

Before we render information about our orders, we need to ensure our order data is fully populated and can be looked up by a current user. Open up your orders context in `lib/hello/orders.ex` and adjust your `get_order!/2` to include a preload:

       def get_order!(%Scope{} = scope, id) do
    -    Repo.get_by!(Order, id: id, user_id: scope.user.id)
    +    Order
    +    |> Repo.get_by!(id: id, user_id: scope.user.id)
    +    |> Repo.preload([line_items: [:product]])
       end

To complete an order, our cart page can issue a POST to the `OrderController.create` action, but we need to implement the operations and logic to actually complete an order. Like before, we'll start at the web interface. Create a new file at `lib/hello_web/controllers/order_controller.ex` and key this in:

    defmodule HelloWeb.OrderController do
      use HelloWeb, :controller

      alias Hello.Orders

      def create(conn, _) do
        case Orders.complete_order(conn.assigns.current_scope, conn.assigns.cart) do
          {:ok, order} ->
            conn
            |> put_flash(:info, "Order created successfully.")
            |> redirect(to: ~p"/orders/#{order}")

          {:error, _reason} ->
            conn
            |> put_flash(:error, "There was an error processing your order")
            |> redirect(to: ~p"/cart")
        end
      end
    end

We wrote the `create` action to call an as-yet-implemented `Orders.complete_order/2` function. Our code is technically "creating" an order, but it's important to step back and consider the naming of your interfaces. The act of completing an order is extremely important in our system. Money changes hands in a transaction, physical goods could be automatically shipped, etc. Such an operation deserves a better, more obvious function name, such as `complete_order`. If the order is completed successfully we redirect to the show page, otherwise a flash error is shown as we redirect back to the cart page.

Here is also a good opportunity to highlight that contexts can naturally work with data defined by other contexts too. This will be especially common with data that is used throughout the application, such as the cart here (but it can also be the current user or the current project, and so forth, depending on your project).

Now we can implement our `Orders.complete_order/2` function. To complete an order, our job will require a few operations:

1. A new order record must be persisted with the total price of the order
2. All items in the cart must be transformed into new order line items records with quantity and point-in-time product price information
3. After successful order insert (and eventual payment), items must be pruned from the cart

From our requirements alone, we can start to see why a generic `create_order` function doesn't cut it. Let's implement this new function in `lib/hello/orders.ex`:

      alias Hello.Orders.LineItem
      alias Hello.ShoppingCart

      def complete_order(%Scope{} = scope, %ShoppingCart.Cart{} = cart) do
        true = cart.user_id == scope.user.id

        line_items =
          Enum.map(cart.items, fn item ->
            %{
              product_id: item.product_id,
              price: item.product.price,
              quantity: item.quantity
            }
          end)

        order_changeset =
          Ecto.Changeset.change(%Order{}, %{
            user_id: scope.user.id,
            total_price: ShoppingCart.total_cart_price(cart),
            line_items: line_items
          })

        Repo.transact(fn ->
          with {:ok, order} <- Repo.insert(order_changeset),
               {:ok, _cart} <- ShoppingCart.prune_cart_items(scope, cart) do
            {:ok, order}
          end
        end)
        |> case do
          {:ok, order} ->
            broadcast_order(scope, {:created, order})
            {:ok, order}

          {:error, reason} ->
            {:error, reason}
        end
      end

We started by mapping the `%ShoppingCart.CartItem{}`'s in our shopping cart into a map of order line items structs. The job of the order line item record is to capture the price of the product at payment transaction time, so we reference the product's price here. Next, we create a bare order changeset with `Ecto.Changeset.change/2` and associate our user UUID, set our total price calculation, and place our order line items in the changeset. With a fresh order changeset ready to be inserted, we now make use of `Repo.transact/2` to execute our operations in a database transaction. We start by inserting the order, followed by a step that prunes all items from the user’s cart. The function wrapped inside `Repo.transact/2` must either return `{:ok, result}` or error, which halts and rolls back the transaction. Running the transaction will execute these operations in sequence, and we return the result to the caller once completed.

To close out our order completion, we need to implement the `ShoppingCart.prune_cart_items/1` function in `lib/hello/shopping_cart.ex`:

      def prune_cart_items(%Scope{} = scope, %Cart{} = cart) do
        {_, _} = Repo.delete_all(from(i in CartItem, where: i.cart_id == ^cart.id))
        {:ok, get_cart(scope)}
      end

Our new function accepts the cart struct and issues a `Repo.delete_all` which accepts a query of all items for the provided cart. We return a success result by simply reloading the pruned cart to the caller. With our context complete, we now need to show the user their completed order. Head back to your order controller and add the `show/2` action:

      def show(conn, %{"id" => id}) do
        order = Orders.get_order!(conn.assigns.current_scope, id)
        render(conn, :show, order: order)
      end

We added the show action to pass our `conn.assigns.current_scope` to `get_order!` which authorizes orders to be viewable only by the owner of the order. Next, we can implement the view and template. Create a new view file at `lib/hello_web/controllers/order_html.ex` with the following content:

    defmodule HelloWeb.OrderHTML do
      use HelloWeb, :html

      embed_templates "order_html/*"
    end

Next we can create the template at `lib/hello_web/controllers/order_html/show.html.heex`:

    <Layouts.app flash={@flash}>
      <.header>
        Thank you for your order!
        <:subtitle>
          <strong>Email: </strong>{@current_scope.user.email}
        </:subtitle>
      </.header>

      <.table id="items" rows={@order.line_items}>
        <:col :let={item} label="Title">{item.product.title}</:col>
        <:col :let={item} label="Quantity">{item.quantity}</:col>
        <:col :let={item} label="Price">
          {HelloWeb.CartHTML.currency_to_str(item.price)}
        </:col>
      </.table>

      <strong>Total price:</strong>
      {HelloWeb.CartHTML.currency_to_str(@order.total_price)}

      <.button navigate={~p"/products"}>Back to products</.button>
    </Layouts.app>

To show our completed order, we displayed the order's user, followed by the line item listing with product title, quantity, and the price we "transacted" when completing the order, along with the total price.

Our last addition will be to add the "complete order" button to our cart page to allow completing an order. Add the following button to the <.header> of the cart show template in `lib/hello_web/controllers/cart_html/show.html.heex`:

      <.header>
        My Cart
    +   <:actions>
    +     <.button href={~p"/orders"} method="post">
    +       Complete order
    +     </.button>
    +   </:actions>
      </.header>

We added a link with `method="post"` to send a POST request to our `OrderController.create` action. If we head back to our cart page at `http://localhost:4000/cart` and complete an order, we'll be greeted by our rendered template:

    Thank you for your order!

    User uuid: 08964c7c-908c-4a55-bcd3-9811ad8b0b9d
    Title                   Quantity Price
    Metaprogramming Elixir  2        $15.00

    Total price: $30.00

We haven't added payments, but we can already see how our `ShoppingCart` and `Orders` context splitting is driving us towards a maintainable solution. With our cart items separated from our order line items, we are well equipped in the future to add payment transactions, cart price detection, and more.

Great work!

← Previous Page 4. Cross-context Boundaries
Next Page → 6. FAQ