From 7ba457120802cafcc2ebde999d9136d8150ecd4f Mon Sep 17 00:00:00 2001 From: Rico Metzger Date: Tue, 4 Jun 2024 20:30:36 +0200 Subject: [PATCH 1/3] Search Criteria improved with sigil --- CHANGELOG.md | 7 ++ README.md | 2 +- lib/bexio_api_client/others.ex | 38 +++++++ lib/bexio_api_client/search_criteria.ex | 32 ++++++ mix.exs | 4 +- test/bexio_api_client/others_test.exs | 59 ++++++++++ .../sales_order_management_test.exs | 59 +++++----- .../bexio_api_client/search_criteria_test.exs | 101 ++++++++++++++++++ 8 files changed, 270 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c15c7e4..ca119fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.5.0 + +* SearchCriteria changed to sigil ~SC with separations for reduced name lengths (old functionality is still there for now) +* Elixir 1.15 as requirement due to ~SC +* Payment Types added +* Bank Accounts added + ## 0.4.2 * Added Github Action to automatically publish a package version diff --git a/README.md b/README.md index 8809e1d..e83abf9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ by adding `bexio_api_client` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:bexio_api_client, "~> 0.4.3"} + {:bexio_api_client, "~> 0.5.0"} ] end ``` diff --git a/lib/bexio_api_client/others.ex b/lib/bexio_api_client/others.ex index b12e425..dbe3a61 100644 --- a/lib/bexio_api_client/others.ex +++ b/lib/bexio_api_client/others.ex @@ -902,4 +902,42 @@ defmodule BexioApiClient.Others do &success_response/2 ) end + + @doc """ + Fetch a list of payment types. + """ + @spec fetch_payment_types( + req :: Req.Request.t(), + opts :: [GlobalArguments.offset_without_order_by_arg()] + ) :: {:ok, map()} | api_error_type() + def fetch_payment_types(req, opts \\ []) do + bexio_body_handling( + fn -> + Req.get(req, url: "/2.0/payment_type", params: opts_to_query(opts)) + end, + &body_to_map/2 + ) + end + + @doc """ + Search a payment type + + Following fields are supported: + + * `name` + """ + @spec search_payment_types( + req :: Req.Request.t(), + criteria :: list(SearchCriteria.t()), + opts :: [GlobalArguments.offset_arg()] + ) :: {:ok, map()} | api_error_type() + def search_payment_types(req, criteria, opts \\ []) do + bexio_body_handling( + fn -> + Req.post(req, url: "/2.0/payment_type/search", json: criteria, params: opts_to_query(opts)) + end, + &body_to_map/2 + ) + end + end diff --git a/lib/bexio_api_client/search_criteria.ex b/lib/bexio_api_client/search_criteria.ex index 66978c1..b14c98b 100644 --- a/lib/bexio_api_client/search_criteria.ex +++ b/lib/bexio_api_client/search_criteria.ex @@ -47,6 +47,8 @@ defmodule BexioApiClient.SearchCriteria do @derive Jason.Encoder defstruct [:field, :criteria, :value] + @regex ~r/^(?\S+)\s*(?!=|=|<=|<|>=|>|in|not in|like|not like|is nil|is not nil)\s*(?.*)$/ + defp new(field, criteria, value) do %__MODULE__{ field: field, @@ -55,6 +57,36 @@ defmodule BexioApiClient.SearchCriteria do } end + @spec sigil_f(String.t(), Keyword.t()) :: t() + def sigil_f(field, opts) + + def sigil_f(expression, _opts) do + case Regex.run(@regex, expression, capture: :all_but_first) do + nil -> raise ArgumentError, "Invalid search criteria: #{expression}" + [field, operator, value] -> handle(field, operator, value) + end + end + + defp handle(field, operator, value) when is_binary(field), + do: handle(String.to_existing_atom(field), operator, value) + + defp handle(field, "=", value) when is_atom(field), do: equal(field, value) + defp handle(field, "!=", value) when is_atom(field), do: not_equal(field, value) + defp handle(field, "<", value) when is_atom(field), do: less_than(field, value) + defp handle(field, "<=", value) when is_atom(field), do: less_equal(field, value) + defp handle(field, ">", value) when is_atom(field), do: greater_than(field, value) + defp handle(field, ">=", value) when is_atom(field), do: greater_equal(field, value) + defp handle(field, "like", value) when is_atom(field), do: like(field, value) + defp handle(field, "not like", value) when is_atom(field), do: not_like(field, value) + defp handle(field, "is nil", _value) when is_atom(field), do: nil?(field) + defp handle(field, "is not nil", _value) when is_atom(field), do: not_nil?(field) + defp handle(field, "in", value) when is_atom(field), do: part_of(field, value) + defp handle(field, "not in", value) when is_atom(field), do: not_part_of(field, value) + + defp handle(field, operator, value), + do: + raise(ArgumentError, "Invalid operator: #{operator} for field #{field} and value #{value}") + @doc "Creates a = search criteria" @spec equal(atom(), any()) :: t() def equal(field, value), do: new(field, :=, value) diff --git a/mix.exs b/mix.exs index d5441c8..30ca05c 100644 --- a/mix.exs +++ b/mix.exs @@ -4,8 +4,8 @@ defmodule BexioApiClient.MixProject do def project do [ app: :bexio_api_client, - version: "0.4.3", - elixir: "~> 1.14", + version: "0.5.0", + elixir: "~> 1.15", start_permanent: Mix.env() == :prod, deps: deps(), package: package(), diff --git a/test/bexio_api_client/others_test.exs b/test/bexio_api_client/others_test.exs index c90c0e3..73f1334 100644 --- a/test/bexio_api_client/others_test.exs +++ b/test/bexio_api_client/others_test.exs @@ -1558,4 +1558,63 @@ defmodule BexioApiClient.OthersTest do assert result == true end end + + describe "fetching a list of payment types" do + setup do + mock_request(fn + %{ + method: "GET", + request_path: "/2.0/payment_type" + } = conn -> + json(conn, [ + %{ + "id" => 1, + "name" => "Cash" + } + ]) + end) + + :ok + end + + test "lists valid results" do + client = BexioApiClient.new("123") + + assert {:ok, result} = BexioApiClient.Others.fetch_payment_types(client) + + assert Enum.count(result) == 1 + assert result[1] == "Cash" + end + end + + describe "searching payment types" do + setup do + mock_request(fn + %{ + method: "POST", + request_path: "/2.0/payment_type/search" + } = conn -> + json(conn, [ + %{ + "id" => 1, + "name" => "Cash" + } + ]) + end) + + :ok + end + + test "lists found results" do + client = BexioApiClient.new("123") + + assert {:ok, result} = + BexioApiClient.Others.search_payment_types(client, [ + SearchCriteria.equal(:name, "Cash") + ]) + + assert Enum.count(result) == 1 + assert result[1] == "Cash" + end + end end diff --git a/test/bexio_api_client/sales_order_management_test.exs b/test/bexio_api_client/sales_order_management_test.exs index c30b7e7..71e6af1 100644 --- a/test/bexio_api_client/sales_order_management_test.exs +++ b/test/bexio_api_client/sales_order_management_test.exs @@ -2166,35 +2166,36 @@ defmodule BexioApiClient.SalesOrderManagementTest do describe "fetching a list of document settings" do setup do - json = Jason.decode! """ - [ - { - "id": 1, - "text": "Quote", - "kb_item_class": "KbOffer", - "enumeration_format": "AN-%nummer%", - "use_automatic_enumeration": true, - "use_yearly_enumeration": false, - "next_nr": 1, - "nr_min_length": 5, - "default_time_period_in_days": 14, - "default_logopaper_id": 1, - "default_language_id": 1, - "default_client_bank_account_new_id": 1, - "default_currency_id": 1, - "default_mwst_type": 0, - "default_mwst_is_net": true, - "default_nb_decimals_amount": 2, - "default_nb_decimals_price": 2, - "default_show_position_taxes": false, - "default_title": "Angebot", - "default_show_esr_on_same_page": false, - "default_payment_type_id": 1, - "kb_terms_of_payment_template_id": 1, - "default_show_total": true - } - ] - """ + json = + Jason.decode!(""" + [ + { + "id": 1, + "text": "Quote", + "kb_item_class": "KbOffer", + "enumeration_format": "AN-%nummer%", + "use_automatic_enumeration": true, + "use_yearly_enumeration": false, + "next_nr": 1, + "nr_min_length": 5, + "default_time_period_in_days": 14, + "default_logopaper_id": 1, + "default_language_id": 1, + "default_client_bank_account_new_id": 1, + "default_currency_id": 1, + "default_mwst_type": 0, + "default_mwst_is_net": true, + "default_nb_decimals_amount": 2, + "default_nb_decimals_price": 2, + "default_show_position_taxes": false, + "default_title": "Angebot", + "default_show_esr_on_same_page": false, + "default_payment_type_id": 1, + "kb_terms_of_payment_template_id": 1, + "default_show_total": true + } + ] + """) mock_request(fn %{method: "GET", request_path: "/2.0/kb_item_setting"} = conn -> diff --git a/test/bexio_api_client/search_criteria_test.exs b/test/bexio_api_client/search_criteria_test.exs index 706cd21..ecf7acd 100644 --- a/test/bexio_api_client/search_criteria_test.exs +++ b/test/bexio_api_client/search_criteria_test.exs @@ -96,6 +96,107 @@ defmodule BexioApiClient.SearchCriteriaTest do test "not_nil? will generate search criteria" do sc = not_nil?(:name) + assert sc.field == :name + assert sc.criteria == :not_null + assert sc.value == nil + end + end + describe "sigils " do + setup do + %{name: "fred"} + end + + test "equal will generate search criteria" do + sc = ~f(name = fred) + + assert sc.field == :name + assert sc.criteria == := + assert sc.value == "fred" + end + + test "not_equal will generate search criteria" do + sc = ~f(name != fred) + + assert sc.field == :name + assert sc.criteria == :!= + assert sc.value == "fred" + end + + test "greater_than will generate search criteria" do + sc = ~f(name > fred) + + assert sc.field == :name + assert sc.criteria == :> + assert sc.value == "fred" + end + + test "less_than will generate search criteria" do + sc = ~f(name < fred) + + assert sc.field == :name + assert sc.criteria == :< + assert sc.value == "fred" + end + + test "greater_equal will generate search criteria" do + sc = ~f(name >= fred) + + assert sc.field == :name + assert sc.criteria == :>= + assert sc.value == "fred" + end + + test "less_equal will generate search criteria" do + sc = ~f(name <= fred) + + assert sc.field == :name + assert sc.criteria == :<= + assert sc.value == "fred" + end + + test "like will generate search criteria" do + sc = ~f(name like fred) + + assert sc.field == :name + assert sc.criteria == :like + assert sc.value == "fred" + end + + test "not_like will generate search criteria" do + sc = ~f(name not like fred) + + assert sc.field == :name + assert sc.criteria == :not_like + assert sc.value == "fred" + end + + test "part_of will generate search criteria" do + sc = ~f(name in fred) + + assert sc.field == :name + assert sc.criteria == :in + assert sc.value == "fred" + end + + test "not_part_of will generate search criteria" do + sc = ~f(name not in fred) + + assert sc.field == :name + assert sc.criteria == :not_in + assert sc.value == "fred" + end + + test "nil? will generate search criteria" do + sc = ~f(name is nil) + + assert sc.field == :name + assert sc.criteria == :is_null + assert sc.value == nil + end + + test "not_nil? will generate search criteria" do + sc = ~f(name is not nil) + assert sc.field == :name assert sc.criteria == :not_null assert sc.value == nil From db2b642020c83df5553206456298638aba212743 Mon Sep 17 00:00:00 2001 From: Rico Metzger Date: Tue, 4 Jun 2024 20:31:48 +0200 Subject: [PATCH 2/3] Added payment types, fixed docs --- CHANGELOG.md | 4 +--- mix.exs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca119fd..ba1884b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,8 @@ ## 0.5.0 -* SearchCriteria changed to sigil ~SC with separations for reduced name lengths (old functionality is still there for now) -* Elixir 1.15 as requirement due to ~SC +* SearchCriteria changed to sigil ~f with separations for reduced name lengths (old functionality is still there for now) * Payment Types added -* Bank Accounts added ## 0.4.2 diff --git a/mix.exs b/mix.exs index 30ca05c..c78df39 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule BexioApiClient.MixProject do [ app: :bexio_api_client, version: "0.5.0", - elixir: "~> 1.15", + elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps(), package: package(), From 2e48887446ccd5738eecb655222abcf15f4786e5 Mon Sep 17 00:00:00 2001 From: Rico Metzger Date: Tue, 4 Jun 2024 20:47:56 +0200 Subject: [PATCH 3/3] Fix search criteria for in and not in --- lib/bexio_api_client/others.ex | 7 ++++-- lib/bexio_api_client/search_criteria.ex | 11 ++++++-- .../bexio_api_client/search_criteria_test.exs | 25 ++++++++++++++++--- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/bexio_api_client/others.ex b/lib/bexio_api_client/others.ex index dbe3a61..71ce554 100644 --- a/lib/bexio_api_client/others.ex +++ b/lib/bexio_api_client/others.ex @@ -934,10 +934,13 @@ defmodule BexioApiClient.Others do def search_payment_types(req, criteria, opts \\ []) do bexio_body_handling( fn -> - Req.post(req, url: "/2.0/payment_type/search", json: criteria, params: opts_to_query(opts)) + Req.post(req, + url: "/2.0/payment_type/search", + json: criteria, + params: opts_to_query(opts) + ) end, &body_to_map/2 ) end - end diff --git a/lib/bexio_api_client/search_criteria.ex b/lib/bexio_api_client/search_criteria.ex index b14c98b..1ec084b 100644 --- a/lib/bexio_api_client/search_criteria.ex +++ b/lib/bexio_api_client/search_criteria.ex @@ -80,13 +80,20 @@ defmodule BexioApiClient.SearchCriteria do defp handle(field, "not like", value) when is_atom(field), do: not_like(field, value) defp handle(field, "is nil", _value) when is_atom(field), do: nil?(field) defp handle(field, "is not nil", _value) when is_atom(field), do: not_nil?(field) - defp handle(field, "in", value) when is_atom(field), do: part_of(field, value) - defp handle(field, "not in", value) when is_atom(field), do: not_part_of(field, value) + defp handle(field, "in", value) when is_atom(field), do: part_of(field, listify(value)) + defp handle(field, "not in", value) when is_atom(field), do: not_part_of(field, listify(value)) defp handle(field, operator, value), do: raise(ArgumentError, "Invalid operator: #{operator} for field #{field} and value #{value}") + defp listify(value) do + case ~r/^\[(?.*)\]$/ |> Regex.named_captures(value) do + %{"value" => value} -> String.split(value, ",") + nil -> [value] + end + end + @doc "Creates a = search criteria" @spec equal(atom(), any()) :: t() def equal(field, value), do: new(field, :=, value) diff --git a/test/bexio_api_client/search_criteria_test.exs b/test/bexio_api_client/search_criteria_test.exs index ecf7acd..934bf8d 100644 --- a/test/bexio_api_client/search_criteria_test.exs +++ b/test/bexio_api_client/search_criteria_test.exs @@ -101,6 +101,7 @@ defmodule BexioApiClient.SearchCriteriaTest do assert sc.value == nil end end + describe "sigils " do setup do %{name: "fred"} @@ -171,19 +172,35 @@ defmodule BexioApiClient.SearchCriteriaTest do end test "part_of will generate search criteria" do - sc = ~f(name in fred) + sc = ~f(name in fred,me) assert sc.field == :name assert sc.criteria == :in - assert sc.value == "fred" + assert sc.value == ["fred,me"] + end + + test "part_of will generate search criteria in list" do + sc = ~f(name in [fred,me]) + + assert sc.field == :name + assert sc.criteria == :in + assert sc.value == ["fred", "me"] end test "not_part_of will generate search criteria" do - sc = ~f(name not in fred) + sc = ~f(name not in fred,me) assert sc.field == :name assert sc.criteria == :not_in - assert sc.value == "fred" + assert sc.value == ["fred,me"] + end + + test "not_part_of will generate search criteria in list" do + sc = ~f(name not in [fred,me]) + + assert sc.field == :name + assert sc.criteria == :not_in + assert sc.value == ["fred", "me"] end test "nil? will generate search criteria" do