Guard in with clause

I’m trying to use a guard in a with clause, as mentioned here:
https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1

Here is a snippet of the code I’m using.

with {:ok, order} <- Repo.insert(changeset),
  {:stripe_ok} when has_payment <- charge(order, payment_method) do
  {:ok, order}
end

Inspecting has_payment shows that it is false, yet the charge method is still executing.

Am I doing something wrong here, or is there a better way to accomplish only calling charge() when has_payment is true?

1 Like

You have the order backwards I think. The guard matches on the ‘match’ step, so charge(...) gets called, it tries to match it to {:stripe_ok}, if that succeeds then it tries the guard of has_payment, and that fails so it bails out.

You probably want it either put it in an if expression, or put it on the line above with {:ok, order}. :slight_smile:

3 Likes
with {:ok, order} <- Repo.insert(changeset),
     true <- has_payment,
     {:stripe_ok} <- charge(order, payment_method) do
  {:ok, order}
end

works as well (though it may offend the more stylistically minded - i.e. it may be judged as a bit noisy).

1 Like

Thanks for the suggestions. I couldn’t get any of them to work correctly, so I ended up passing the has_payment var to the charge() function

def charge(_order, false, _payment_method), do: {:stripe_ok}
def charge(order, true, payment_method) do
  ...
end

I wanted to avoid this because it’s weird to return [:stripe_ok} when stripe is never called, but it is working correctly.

From your original code I assumed that the with was supposed to fail when has_payment != true (while Repo.insert was still invoked beforehand) - which apparently wasn’t your intention.

1 Like

Ah yea, sorry for the confusion. My goal (which hopefully is clearer now) was to:

  1. Save an order
  2. Charge a payment, when one is passed

If there is a payment, and it fails, I want the order to rollback.
If there is no payment, then I want it to skip the charge() method and just save the order.

I would use Ecto.Multi to handle this. You can control how each event is handled and whether it rolls back the transaction or proceeds. For example, below, if the payment fails, the function returns {:error, _} which rolls everything back. But if there is no payment, it still returns {:ok, _} and will skip the payment and save the order.

Checkout this article for a good application of this.

def place_order(changeset, has_payment, payment_method) do 
  Multi.new()
  |> Multi.run(:save_order, &insert_order(&1, changeset))
  |> Multi.run(:check_payment, &check_for_payment(&1, has_payment))
  |> Multi.run(:charge, &charge_with_stripe(&1, payment_method))
  |> Repo.transaction()
end

defp insert_order(_multi, changeset) do
  Repo.insert(changeset)
end

defp check_for_payment(_multi, true), do: {:ok, true}
defp check_for_payment(_multi, false), do: {:ok, false}

defp charge_with_stripe(%{check_for_payment: false}, _payment_method), do: {:ok, :skipped}
defp charge_with_stripe(%{save_order: order}, payment_method) do
  case charge(order, payment_method) do
    :stripe_ok ->
      {:ok, "Payment processed"}
    _ ->
      {:error, "Payment failed"
  end
end
3 Likes

Is the guard in a with a real guard or can it be any test? Is has_payment() a normal function, or is has_payment a variable?

I assumed is_payment Is a variable. An experiment with an anonymous function resulted in

** (CompileError) iex:54: invalid expression in guard, anonymous call is not allowed in guards
    (stdlib) lists.erl:1354: :lists.mapfoldl/3

So it does seem to be indeed an actual guard (not just look like one).

1 Like