Phoenix / Ecto - Ecto Query Composition

Building upon Existing Queries

defmodule GaraioRemGraphqlWeb.Resolvers.Kreditorenauftrag do
  alias GaraioRemGraphql.Repo
  alias GaraioRemGraphql.Rechnungswesen
  alias GaraioRemGraphql.Rechnungswesen.Kreditorenauftrag
  alias GaraioRemGraphqlWeb.Resolvers.Internal.Errors

  import Ecto.Query, warn: false
  import GaraioRemGraphql.Util.Pagination, only: [validate_cursor: 1]

  # Deprecated
  def all(_args, %{context: %{client_info: client_info}}) do
    {:ok, Rechnungswesen.kreditorenauftraege_fuer(client_info) |> Repo.all()}
  end

  # Deprecated
  def all(_args, _info) do
    {:error,
     %{
       message: "not authenticated",
       extensions: %{code: "not_authenticated"}
     }}
  end

  def all_paginated(args, %{context: %{client_info: client_info}}) do
    with {:ok, _decoded_cursor} <- validate_cursor(args[:cursor]),
         %{entries: entries, metadata: metadata} <-
           Repo.paginate(
             Rechnungswesen.kreditorenauftraege_fuer(client_info),
             cursor_fields: [:referenz],
             after: args[:cursor],
             limit: Application.fetch_env!(:garaio_rem_graphql, :page_size)
           ) do
      {:ok, %{page: entries, cursor: metadata.after}}
    end
  end

  def all_paginated(_args, _info) do
    {:error,
     %{
       message: "not authenticated",
       extensions: %{code: "not_authenticated"}
     }}
  end

  def find_supplier_order(
        %{supplier_reference: supplier_reference} = search_args,
        %{context: %{client_info: _client_info}}
      ) do
    with {:ok, order} <- resolve_unique_supplier_order(supplier_reference, search_args) do
      {:ok, preload_order_associations(order)}
    else
      _ ->
        Errors.no_unique_match(search_args)
    end
  end

  defp resolve_unique_supplier_order(supplier_reference, search_args) do
    base_query = core_query(supplier_reference)

    with {:error, :no_unique_match} <- external_reference_search(base_query, search_args),
         {:error, :no_unique_match} <- internal_order_reference_search(base_query, search_args),
         {:error, :no_unique_match} <- masterdata_reference_search(base_query, search_args),
         {:error, :no_unique_match} <- order_detail_description_search(base_query, search_args) do
      {:error, :no_unique_match}
    else
      {:ok, order} -> {:ok, order}
      _ -> {:error, :no_unique_match}
    end
  end

  defp external_reference_search(base_query, search_args) do
    with %{external_order_reference: external_order_reference}
         when is_binary(external_order_reference) <- search_args,
         [order] <- external_reference_query(base_query, external_order_reference) |> Repo.all() do
      {:ok, order}
    else
      _ -> {:error, :no_unique_match}
    end
  end

  defp internal_order_reference_search(base_query, search_args) do
    with %{internal_order_reference: internal_order_reference}
         when is_binary(internal_order_reference) <- search_args,
         [order] <- internal_reference_query(base_query, internal_order_reference) |> Repo.all() do
      {:ok, order}
    else
      _ -> {:error, :no_unique_match}
    end
  end

  defp masterdata_reference_search(base_query, search_args) do
    with %{masterdata_reference: masterdata_reference}
         when is_binary(masterdata_reference) <- search_args,
         [order] <- masterdata_reference_query(base_query, masterdata_reference) |> Repo.all() do
      {:ok, order}
    else
      _ -> {:error, :no_unique_match}
    end
  end

  defp order_detail_description_search(base_query, search_args) do
    with %{order_detail_description: order_detail_description}
         when is_binary(order_detail_description) <- search_args,
         [order] <- fuzzy_description_logic(base_query, order_detail_description) do
      {:ok, order}
    else
      _ -> {:error, :no_unique_match}
    end
  end

  defp fuzzy_description_logic(base_query, order_detail_description) do
    fuzzy_results =
      fuzzy_description_query(base_query, order_detail_description)
      |> Repo.all()

    case fuzzy_results do
      [one_order] ->
        # if there is only one - return only the oder (without the associated score)
        [one_order.order]

      fuzzy_results when length(fuzzy_results) > 1 ->
        [first | _rest] = fuzzy_results
        first_score = first.score

        # find orders with the top score - return only the oder (without the associated score)
        fuzzy_results
        |> Enum.filter(fn %{score: score} -> score == first_score end)
        |> Enum.map(fn %{order: order} -> order end)

      _ ->
        # no results or unexpected results
        fuzzy_results
    end
  end

  defp preload_order_associations(order) do
    order
    |> Repo.preload([
      :kreditor,
      :buchhaltung,
      :kreditorenauftrag_positionen,
      detail: [:verwaltungseinheit, :haus, :objekt]
    ])
  end

  defp core_query(supplier_reference) do
    from(order in Kreditorenauftrag,
      join: supplier in assoc(order, :kreditor),
      join: account in assoc(order, :buchhaltung),
      left_join: detail in assoc(order, :detail),
      left_join: verwaltungseinheit in assoc(detail, :verwaltungseinheit),
      left_join: haus in assoc(detail, :haus),
      left_join: objekt in assoc(detail, :objekt),
      left_join: positions in assoc(order, :kreditorenauftrag_positionen),
      limit: 2,
      where:
        order.aasm_state == "erledigt" and
          order.type == "Kreditorenauftrag" and
          is_nil(detail.kreditor_rechnung_id) and
          supplier.referenz == ^supplier_reference
    )
  end

  defp external_reference_query(base_query, external_order_reference) do
    from(order in base_query,
      where: order.externe_rechnungs_nr == ^external_order_reference
    )
  end

  defp internal_reference_query(base_query, internal_order_reference) do
    from(order in base_query,
      where: order.referenz == ^internal_order_reference
    )
  end

  defp masterdata_reference_query(base_query, masterdata_reference) do
    from(order in base_query,
      left_join: detail in assoc(order, :detail),
      left_join: verwaltungseinheit in assoc(detail, :verwaltungseinheit),
      left_join: haus in assoc(detail, :haus),
      left_join: objekt in assoc(detail, :objekt),
      where:
        haus.referenz == ^masterdata_reference or
          objekt.referenz == ^masterdata_reference or
          verwaltungseinheit.referenz == ^masterdata_reference
    )
  end

  defp fuzzy_description_query(base_query, order_detail_description, similarity_threshold \\ 0.3) do
    subquery =
      from(order in base_query,
        left_join: detail in assoc(order, :detail),
        where:
          fragment(
            "word_similarity(?, ?) > ?",
            detail.betreff,
            ^order_detail_description,
            ^similarity_threshold
          ),
        select: %{
          id: order.id,
          score:
            fragment(
              "word_similarity(?, ?)",
              detail.betreff,
              ^order_detail_description
            )
        },
        distinct: order.id
      )

    # IMPORTANT: the mainquery with subquery ensures that the order is done AFTER the distinct by id in subquery
    from(s in subquery(subquery),
      join: k in Kreditorenauftrag,
      on: s.id == k.id,
      order_by: [desc: s.score],
      select: %{
        order: k,
        score: s.score
      },
      limit: 2
    )
  end
end
Bill Tihen
Bill Tihen
Developer, Data Enthusiast, Educator and Nature’s Friend

very curious – known to explore knownledge and nature