diff --git a/lib/ash_money/ash_postgres_extension.ex b/lib/ash_money/ash_postgres_extension.ex index 525aab2..5503b11 100644 --- a/lib/ash_money/ash_postgres_extension.ex +++ b/lib/ash_money/ash_postgres_extension.ex @@ -3,10 +3,23 @@ if Code.ensure_loaded?(AshPostgres.CustomExtension) do @moduledoc """ Installs the `money_with_currency` type and operators/functions for Postgres. """ - use AshPostgres.CustomExtension, name: :ash_money, latest_version: 3 + use AshPostgres.CustomExtension, name: :ash_money, latest_version: 4 + + def install(3) do + """ + #{Money.DDL.execute_each(add_money_greater_than())} + #{Money.DDL.execute_each(add_money_greater_than_or_equal())} + #{Money.DDL.execute_each(add_money_less_than())} + #{Money.DDL.execute_each(add_money_less_than_or_equal())} + """ + end def install(2) do """ + #{Money.DDL.execute_each(add_money_greater_than())} + #{Money.DDL.execute_each(add_money_greater_than_or_equal())} + #{Money.DDL.execute_each(add_money_less_than())} + #{Money.DDL.execute_each(add_money_less_than_or_equal())} #{Money.DDL.execute_each(add_money_sub())} #{Money.DDL.execute_each(add_money_neg())} """ @@ -14,6 +27,10 @@ if Code.ensure_loaded?(AshPostgres.CustomExtension) do def install(1) do """ + #{Money.DDL.execute_each(add_money_greater_than())} + #{Money.DDL.execute_each(add_money_greater_than_or_equal())} + #{Money.DDL.execute_each(add_money_less_than())} + #{Money.DDL.execute_each(add_money_less_than_or_equal())} #{Money.DDL.execute_each(add_money_sub())} #{Money.DDL.execute_each(add_money_mult())} #{Money.DDL.execute_each(add_money_neg())} @@ -22,6 +39,10 @@ if Code.ensure_loaded?(AshPostgres.CustomExtension) do def install(0) do """ + #{Money.DDL.execute_each(add_money_greater_than())} + #{Money.DDL.execute_each(add_money_greater_than_or_equal())} + #{Money.DDL.execute_each(add_money_less_than())} + #{Money.DDL.execute_each(add_money_less_than_or_equal())} #{Money.DDL.execute_each(Money.DDL.create_money_with_currency())} #{Money.DDL.execute_each(add_money_sub())} #{Money.DDL.execute_each(add_money_neg())} @@ -32,6 +53,16 @@ if Code.ensure_loaded?(AshPostgres.CustomExtension) do """ end + def uninstall(4) do + """ + #{Money.DDL.execute_each(remove_money_greater_than())} + #{Money.DDL.execute_each(remove_money_greater_than_or_equal())} + #{Money.DDL.execute_each(remove_money_less_than())} + #{Money.DDL.execute_each(remove_money_less_than_or_equal())} + #{uninstall(3)} + """ + end + def uninstall(3) do """ #{Money.DDL.execute_each(remove_money_sub())} @@ -50,6 +81,298 @@ if Code.ensure_loaded?(AshPostgres.CustomExtension) do """ end + defp add_money_greater_than do + """ + CREATE OR REPLACE FUNCTION money_gt(money_1 money_with_currency, money_2 money_with_currency) + RETURNS BOOLEAN + IMMUTABLE + STRICT + LANGUAGE plpgsql + AS $$ + DECLARE + currency varchar; + result boolean; + BEGIN + IF currency_code(money_1) = currency_code(money_2) THEN + currency := currency_code(money_1); + result := amount(money_1) > amount(money_2); + return result; + ELSE + RAISE EXCEPTION + 'Incompatible currency codes for > operator. Expected both currency codes to be %', currency_code(money_1) + USING HINT = 'Please ensure both columns have the same currency code', + ERRCODE = '22033'; + END IF; + END; + $$; + + + CREATE OR REPLACE FUNCTION money_gt(money_1 money_with_currency, amount numeric) + RETURNS BOOLEAN + IMMUTABLE + STRICT + LANGUAGE plpgsql + AS $$ + DECLARE + currency varchar; + result boolean; + BEGIN + currency := currency_code(money_1); + result := amount(money_1) > amount; + return result; + END; + $$; + + + CREATE OPERATOR > ( + leftarg = money_with_currency, + rightarg = money_with_currency, + procedure = money_gt + ); + + + CREATE OPERATOR > ( + leftarg = money_with_currency, + rightarg = numeric, + procedure = money_gt + ); + """ + end + + defp add_money_greater_than_or_equal do + """ + CREATE OR REPLACE FUNCTION money_gte(money_1 money_with_currency, money_2 money_with_currency) + RETURNS BOOLEAN + IMMUTABLE + STRICT + LANGUAGE plpgsql + AS $$ + DECLARE + currency varchar; + result boolean; + BEGIN + IF currency_code(money_1) = currency_code(money_2) THEN + currency := currency_code(money_1); + result := amount(money_1) >= amount(money_2); + return result; + ELSE + RAISE EXCEPTION + 'Incompatible currency codes for >= operator. Expected both currency codes to be %', currency_code(money_1) + USING HINT = 'Please ensure both columns have the same currency code', + ERRCODE = '22033'; + END IF; + END; + $$; + + + CREATE OR REPLACE FUNCTION money_gte(money_1 money_with_currency, amount numeric) + RETURNS BOOLEAN + IMMUTABLE + STRICT + LANGUAGE plpgsql + AS $$ + DECLARE + currency varchar; + result boolean; + BEGIN + currency := currency_code(money_1); + result := amount(money_1) >= amount; + return result; + END; + $$; + + + CREATE OPERATOR >= ( + leftarg = money_with_currency, + rightarg = money_with_currency, + procedure = money_gt + ); + + + CREATE OPERATOR >= ( + leftarg = money_with_currency, + rightarg = numeric, + procedure = money_gt + ); + """ + end + + defp add_money_less_than do + """ + CREATE OR REPLACE FUNCTION money_lt(money_1 money_with_currency, money_2 money_with_currency) + RETURNS BOOLEAN + IMMUTABLE + STRICT + LANGUAGE plpgsql + AS $$ + DECLARE + currency varchar; + result boolean; + BEGIN + IF currency_code(money_1) = currency_code(money_2) THEN + currency := currency_code(money_1); + result := amount(money_1) < amount(money_2); + return result; + ELSE + RAISE EXCEPTION + 'Incompatible currency codes for < operator. Expected both currency codes to be %', currency_code(money_1) + USING HINT = 'Please ensure both columns have the same currency code', + ERRCODE = '22033'; + END IF; + END; + $$; + + + CREATE OR REPLACE FUNCTION money_lt(money_1 money_with_currency, amount numeric) + RETURNS BOOLEAN + IMMUTABLE + STRICT + LANGUAGE plpgsql + AS $$ + DECLARE + currency varchar; + result boolean; + BEGIN + currency := currency_code(money_1); + result := amount(money_1) < amount; + return result; + END; + $$; + + + CREATE OPERATOR < ( + leftarg = money_with_currency, + rightarg = money_with_currency, + procedure = money_lt + ); + + + CREATE OPERATOR < ( + leftarg = money_with_currency, + rightarg = numeric, + procedure = money_lt + ); + """ + end + + defp add_money_less_than_or_equal do + """ + CREATE OR REPLACE FUNCTION money_lte(money_1 money_with_currency, money_2 money_with_currency) + RETURNS BOOLEAN + IMMUTABLE + STRICT + LANGUAGE plpgsql + AS $$ + DECLARE + currency varchar; + result boolean; + BEGIN + IF currency_code(money_1) = currency_code(money_2) THEN + currency := currency_code(money_1); + result := amount(money_1) <= amount(money_2); + return result; + ELSE + RAISE EXCEPTION + 'Incompatible currency codes for <= operator. Expected both currency codes to be %', currency_code(money_1) + USING HINT = 'Please ensure both columns have the same currency code', + ERRCODE = '22033'; + END IF; + END; + $$; + + + CREATE OR REPLACE FUNCTION money_lte(money_1 money_with_currency, amount numeric) + RETURNS BOOLEAN + IMMUTABLE + STRICT + LANGUAGE plpgsql + AS $$ + DECLARE + currency varchar; + result boolean; + BEGIN + currency := currency_code(money_1); + result := amount(money_1) <= amount; + return result; + END; + $$; + + + CREATE OPERATOR <= ( + leftarg = money_with_currency, + rightarg = money_with_currency, + procedure = money_lte + ); + + + CREATE OPERATOR <= ( + leftarg = money_with_currency, + rightarg = numeric, + procedure = money_lte + ); + """ + end + + defp remove_money_greater_than do + """ + DROP OPERATOR >(money_with_currency, money_with_currency); + + + DROP OPERATOR >(money_with_currency, numeric); + + + DROP FUNCTION IF EXISTS money_gt(money_1 money_with_currency, money_2 money_with_currency); + + + DROP FUNCTION IF EXISTS money_gt(money_1 money_with_currency, amount numeric); + """ + end + + defp remove_money_greater_than_or_equal do + """ + DROP OPERATOR >=(money_with_currency, money_with_currency); + + + DROP OPERATOR >=(money_with_currency, numeric); + + + DROP FUNCTION IF EXISTS money_gte(money_1 money_with_currency, money_2 money_with_currency); + + + DROP FUNCTION IF EXISTS money_gte(money_1 money_with_currency, amount numeric); + """ + end + + defp remove_money_less_than do + """ + DROP OPERATOR <(money_with_currency, money_with_currency); + + + DROP OPERATOR <(money_with_currency, numeric); + + + DROP FUNCTION IF EXISTS money_lt(money_1 money_with_currency, money_2 money_with_currency); + + + DROP FUNCTION IF EXISTS money_lt(money_1 money_with_currency, amount numeric); + """ + end + + defp remove_money_less_than_or_equal do + """ + DROP OPERATOR <=(money_with_currency, money_with_currency); + + + DROP OPERATOR <=(money_with_currency, numeric); + + + DROP FUNCTION IF EXISTS money_lte(money_1 money_with_currency, money_2 money_with_currency); + + + DROP FUNCTION IF EXISTS money_lte(money_1 money_with_currency, amount numeric); + """ + end + defp add_money_neg do """ CREATE OR REPLACE FUNCTION money_neg(money_1 money_with_currency) diff --git a/lib/ash_money/types/money.ex b/lib/ash_money/types/money.ex index 16762ab..c8b1b39 100644 --- a/lib/ash_money/types/money.ex +++ b/lib/ash_money/types/money.ex @@ -39,16 +39,101 @@ defmodule AshMoney.Types.Money do [__MODULE__, __MODULE__] => __MODULE__ }, :- => %{ - [__MODULE__, __MODULE__] => __MODULE__ + [__MODULE__] => __MODULE__ }, :* => %{ [__MODULE__, :integer] => __MODULE__, + [__MODULE__, :decimal] => __MODULE__, + [:integer, __MODULE__] => __MODULE__, + [:decimal, __MODULE__] => __MODULE__ + }, + :< => %{ + [__MODULE__, :integer] => __MODULE__, + [__MODULE__, :decimal] => __MODULE__, + [__MODULE__, __MODULE__] => __MODULE__, + [:decimal, __MODULE__] => __MODULE__, + [:integer, __MODULE__] => __MODULE__ + }, + :<= => %{ + [__MODULE__, :integer] => __MODULE__, + [__MODULE__, :decimal] => __MODULE__, + [__MODULE__, __MODULE__] => __MODULE__, + [:decimal, __MODULE__] => __MODULE__, + [:integer, __MODULE__] => __MODULE__ + }, + :> => %{ + [__MODULE__, :integer] => __MODULE__, + [__MODULE__, :decimal] => __MODULE__, + [__MODULE__, __MODULE__] => __MODULE__, + [:decimal, __MODULE__] => __MODULE__, + [:integer, __MODULE__] => __MODULE__ + }, + :>= => %{ + [__MODULE__, :integer] => __MODULE__, + [__MODULE__, :decimal] => __MODULE__, + [__MODULE__, __MODULE__] => __MODULE__, + [:decimal, __MODULE__] => __MODULE__, [:integer, __MODULE__] => __MODULE__ } } end @impl true + def matches_type?(%Money{}, _), do: true + def matches_type?(_, _), do: false + + @impl true + def evaluate_operator(%op{ + left: %Money{} = left, + right: %Money{} = right + }) + when op in [ + Ash.Query.Operator.LessThan, + Ash.Query.Operator.GreaterThan, + Ash.Query.Operator.LessThanOrEqual, + Ash.Query.Operator.GreaterThanOrEqual + ] do + requirement = + case op do + Ash.Query.Operator.LessThan -> [:lt] + Ash.Query.Operator.GreaterThan -> [:gt] + Ash.Query.Operator.LessThanOrEqual -> [:lt, :eq] + Ash.Query.Operator.GreaterThanOrEqual -> [:gt, :eq] + end + + Money.compare!(left, right) in requirement + end + + def evaluate_operator( + %op{ + left: %Money{} = left, + right: right + } = operator + ) + when op in [ + Ash.Query.Operator.LessThan, + Ash.Query.Operator.GreaterThan, + Ash.Query.Operator.LessThanOrEqual, + Ash.Query.Operator.GreaterThanOrEqual + ] do + evaluate_operator(%{operator | left: left, right: Money.new(left.currency, right)}) + end + + def evaluate_operator( + %op{ + left: left, + right: %Money{} = right + } = operator + ) + when op in [ + Ash.Query.Operator.LessThan, + Ash.Query.Operator.GreaterThan, + Ash.Query.Operator.LessThanOrEqual, + Ash.Query.Operator.GreaterThanOrEqual + ] do + evaluate_operator(%{operator | left: Money.new(right.currency, left), right: right}) + end + def evaluate_operator(%Ash.Query.Operator.Basic.Plus{ left: %Money{} = left, right: %Money{} = right diff --git a/mix.exs b/mix.exs index 7999661..605f2a3 100644 --- a/mix.exs +++ b/mix.exs @@ -103,7 +103,7 @@ defmodule AshMoney.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ash, ash_version("~> 3.0")}, + {:ash, ash_version("~> 3.0 and >= 3.0.15")}, {:ex_money, "~> 5.15"}, {:ex_money_sql, "~> 1.0", optional: true}, {:ash_postgres, "~> 2.0", optional: true}, diff --git a/mix.lock b/mix.lock index d8866a3..a5f5de2 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "absinthe": {:hex, :absinthe, "1.7.6", "0b897365f98d068cfcb4533c0200a8e58825a4aeeae6ec33633ebed6de11773b", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7626951ca5eec627da960615b51009f3a774765406ff02722b1d818f17e5778"}, "absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"}, - "ash": {:hex, :ash, "3.0.14", "0a76b8574a9bda07aacf4ab8c3cc5dd8a48bb76944d14a7f30165646cd072138", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, ">= 0.8.1 and < 1.0.0-0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a2ee23bb84b6abfafb1bdb6b0bb0bcd373c84947ce53efb1e83a22bd2f3452ad"}, + "ash": {:hex, :ash, "3.0.15", "1cea8ca799dc8281d09e316a189fddfb27a2a29a0b0e5af369aa83d6f731ca75", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, ">= 0.8.1 and < 1.0.0-0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "97abfed57f2bf29c3889c51f0dfa23b382a1361a9d70e7d82d7e59a2be6bdc73"}, "ash_graphql": {:hex, :ash_graphql, "1.2.0", "b4b7a754ef722cff1c84cf35291e2ff0402fc91d805e2a01405157087f908a9b", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d16986527788f74b2fe8085827d81bad08f1574d8562bc52619c00d43e75aa52"}, "ash_postgres": {:hex, :ash_postgres, "2.0.9", "e6036512e16e672b80c2a0154f3bbd4828b8a6619c03b97c81c3df51ee902023", [:mix], [{:ash, ">= 3.0.7 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.4 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "a362fd8a7922e0c21d8dccd6b5fd177b5ea62dbb223bbf04f92fed678d7cc405"}, "ash_sql": {:hex, :ash_sql, "0.2.5", "8b50c3178776263b912e1b60e161e2bcf08a907a38abf703edf8a8a0a51b3fe2", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "0d5d8606738a17c4e8c0be4244623df721abee5072cee69d31c2711c36d0548f"},