Git Product home page Git Product logo

money_sql's Introduction

Introduction to Money SQL

Hex.pm Hex.pm Hex.pm Hex.pm

Money_SQL implements a set of functions to store and retrieve data structured as a %Money{} type that is composed of an ISO 4217 currency code and a currency amount. See ex_money for details of using Money. Note that ex_money_sql depends on ex_money.

Embedded Schema Configuration from ex_money_sql 1.9.2 {: .warning}

Please ensure that if you are using Ecto embedded schemas that include a money type that it is configured with the type Money.Ecto.Map.Type, NOT Money.Ecto.Composite.Type.

In previous releases the misconfiguration of the type worked by accident. From ex_money_sql version 1.9.2 and subsequent releases an exception like ** (Protocol.UndefinedError) protocol Jason.Encoder not implemented for {"USD", Decimal.new("50.00")} of type Tuple will be raised. This is most likely an indication of type misconfiguration in an embedded schema.

Installation

ex_money_sql can be installed by adding ex_money_sql to your list of dependencies in mix.exs and then executing mix deps.get

def deps do
  [
    {:ex_money_sql, "~> 1.0"},
    ...
  ]
end

Note that ex_money_sql is supported on Elixir 1.11 and later only.

Serializing to a Postgres database with Ecto

Money_SQL provides custom Ecto data types and a custom Postgres data type to provide serialization of Money.t types without losing precision whilst also maintaining the integrity of the {currency_code, amount} relationship. To serialise and retrieve money types from a database the following steps should be followed:

  1. First generate the migration to create the custom type:
mix money.gen.postgres.money_with_currency
* creating priv/repo/migrations
* creating priv/repo/migrations/20161007234652_add_money_with_currency_type_to_postgres.exs
  1. Then migrate the database:
mix ecto.migrate
07:09:28.637 [info]  == Running MoneyTest.Repo.Migrations.AddMoneyWithCurrencyTypeToPostgres.up/0 forward
07:09:28.640 [info]  execute "CREATE TYPE public.money_with_currency AS (currency_code char(3), amount numeric)"
07:09:28.647 [info]  == Migrated in 0.0s
  1. Create your database migration with the new type (don't forget to mix ecto.migrate as well):
defmodule MoneyTest.Repo.Migrations.CreateLedger do
  use Ecto.Migration

  def change do
    create table(:ledgers) do
      add :amount, :money_with_currency
      timestamps()
    end
  end
end
  1. Create your schema using the Money.Ecto.Composite.Type ecto type:
defmodule Ledger do
  use Ecto.Schema

  schema "ledgers" do
    field :amount, Money.Ecto.Composite.Type

    timestamps()
  end
end
  1. Insert into the database:
iex> Repo.insert %Ledger{amount: Money.new(:USD, "100.00")}
[debug] QUERY OK db=4.5ms
INSERT INTO "ledgers" ("amount","inserted_at","updated_at") VALUES ($1,$2,$3)
[{"USD", #Decimal<100.00>}, {{2016, 10, 7}, {23, 12, 13, 0}}, {{2016, 10, 7}, {23, 12, 13, 0}}]
  1. Retrieve from the database:
iex> Repo.all Ledger
[debug] QUERY OK source="ledgers" db=5.3ms decode=0.1ms queue=0.1ms
SELECT l0."amount", l0."inserted_at", l0."updated_at" FROM "ledgers" AS l0 []
[%Ledger{__meta__: #Ecto.Schema.Metadata<:loaded, "ledgers">, amount: #<:USD, 100.00>,
  inserted_at: ~N[2017-02-21 00:15:40.979576],
  updated_at: ~N[2017-02-21 00:15:40.991391]}]

Serializing to a MySQL (or other non-Postgres) database with Ecto

Since MySQL does not support composite types, the :map type is used which in MySQL is implemented as a JSON column. The currency code and amount are serialised into this column.

defmodule MoneyTest.Repo.Migrations.CreateLedger do
  use Ecto.Migration

  def change do
    create table(:ledgers) do
      add :amount, :map
      timestamps()
    end
  end
end

Create your schema using the Money.Ecto.Map.Type ecto type:

defmodule Ledger do
  use Ecto.Schema

  schema "ledgers" do
    field :amount, Money.Ecto.Map.Type

    timestamps()
  end
end

Insert into the database:

iex> Repo.insert %Ledger{amount_map: Money.new(:USD, 100)}
[debug] QUERY OK db=25.8ms
INSERT INTO "ledgers" ("amount_map","inserted_at","updated_at") VALUES ($1,$2,$3)
RETURNING "id" [%{amount: "100", currency: "USD"},
{{2017, 2, 21}, {0, 15, 40, 979576}}, {{2017, 2, 21}, {0, 15, 40, 991391}}]

{:ok,
 %MoneyTest.Thing{__meta__: #Ecto.Schema.Metadata<:loaded, "ledgers">,
  amount: nil, amount_map: #Money<:USD, 100>, id: 3,
  inserted_at: ~N[2017-02-21 00:15:40.979576],
  updated_at: ~N[2017-02-21 00:15:40.991391]}}

Retrieve from the database:

iex> Repo.all Ledger
[debug] QUERY OK source="ledgers" db=16.1ms decode=0.1ms
SELECT t0."id", t0."amount_map", t0."inserted_at", t0."updated_at" FROM "ledgers" AS t0 []
[%Ledger{__meta__: #Ecto.Schema.Metadata<:loaded, "ledgers">,
  amount_map: #Money<:USD, 100>, id: 3,
  inserted_at: ~N[2017-02-21 00:15:40.979576],
  updated_at: ~N[2017-02-21 00:15:40.991391]}]

Notes:

  1. In order to preserve precision of the decimal amount, the amount part of the %Money{} struct is serialised as a string. This is done because JSON serializes numeric values as either integer or float, neither of which would preserve precision of a decimal value.

  2. The precision of the serialized string value of amount is affected by the setting of Decimal.get_context. The default is 28 digits which should cater for your requirements.

  3. Serializing the amount as a string means that SQL query arithmetic and equality operators will not work as expected. You may find that CASTing the string value will restore some of that functionality. For example:

CAST(JSON_EXTRACT(amount_map, '$.amount') AS DECIMAL(20, 8)) AS amount;

Casting Money with Changesets

Then the schema type is Money.Ecto.Composite.Type then any option that is applicable to Money.parse/2 or Money.new/3 can be added to the field definition. These options will then be applied when Money.Ecto.Composite.Type.cast/2 or Money.Ecto.Composite.Type.load/3 is called. These functions are called with loading data from the database or when calling Ecto.Changeset.cast/3 is called. Typically this is useful to:

  1. Apply a default currency to a field input representing a money amount.
  2. Add formatting options to the returned t:Money that will be applied when calling Money.to_string/2

Consider the following example where a money amount will be considered in a default currency if no currency is applied:

Schema Example

The example below has three columns defined as Money.Ecto.Composite.Type.

  • :payroll will be cast as with the default currency :JPY if no currency field is provided. Note that if no :default_currency option is defined, the default currency will be derived from the current locale or configured :locale option.

  • :tax is defined with the option :fractional_digits. This option will be applied when formatting :tax with Money.to_string/2

  • :default is the t:Money that is used if the :value field is nil both when casting and when loading from the database.

defmodule Organization do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key false
  schema "organizations" do
    field :payroll,         Money.Ecto.Composite.Type, default_currency: :JPY
    field :tax,             Money.Ecto.Composite.Type, fractional_digits: 4
    field :value,           Money.Ecto.Composite.Type, default: Money.new(:USD, 0)
    field :name,            :string
    field :employee_count,  :integer
    timestamps()
  end

  def changeset(organization, params \\ %{}) do
    organization
    |> cast(params, [:payroll])
  end
end

Embedded schema example

Embedded schemas are represented in Postgres as a jsobn data type which, in Elixir, is represented as a map. Therefore to include money fields in an embedded scheam, the Money.Ecto.Map.Type is used. Here is an example schema, extending the previous example:

defmodule Organization do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key false
  schema "organizations" do
    field :payroll,         Money.Ecto.Composite.Type, default_currency: :JPY
    field :tax,             Money.Ecto.Composite.Type, fractional_digits: 4
    field :value,           Money.Ecto.Composite.Type, default: Money.new(:USD, 0)
    field :name,            :string
    field :employee_count,  :integer
    embeds_many :customers, Customer do
      field :name, :string
      field :revenue, Money.Ecto.Map.Type, default: Money.new(:USD, 0)
    end
    timestamps()
  end

Changeset execution

In the following example, a default of :JPY currency (using our previous schema example) will be applied when casting the changeset.

iex> changeset = Organization.changeset(%Organization{}, %{payroll: "0"})
iex> changeset.changes.payroll == Money.new(:JPY, 0)
true

Postgres Database functions

Since the datatype used to store Money in Postgres is a composite type (called :money_with_currency), the standard aggregation functions like sum and average are not supported and the order_by clause doesn't perform as expected. Money provides mechanisms to provide these functions.

Plus operator +

Money defines a migration generator which, when migrated to the database with mix ecto.migrate, supports the + operator for :money_with_currency columns. The steps are:

  1. Generate the migration by executing mix money.gen.postgres.plus_operator

  2. Migrate the database by executing mix ecto.migrate

  3. Formulate an Ecto query to use the + operator

  iex> q = Ecto.Query.select Item, [l], type(fragment("price + price"), l.price)
  #Ecto.Query<from l0 in Item, select: type(fragment("price + price"), l0.price)>
  iex> Repo.one q
  [debug] QUERY OK source="items" db=5.6ms queue=0.5ms
  SELECT price + price::money_with_currency FROM "items" AS l0 []
  #Money<:USD, 200>]

Aggregate functions: sum()

Money provides a migration generator which, when migrated to the database with mix ecto.migrate, supports performing sum() aggregation on Money types. The steps are:

  1. Generate the migration by executing mix money.gen.postgres.sum_function

  2. Migrate the database by executing mix ecto.migrate

  3. Formulate an Ecto query to use the aggregate function sum()

  # Formulate the query.  Note the required use of the type()
  # expression which is needed to inform Ecto of the return
  # type of the function
  iex> q = Ecto.Query.select Item, [l], type(sum(l.price), l.price)
  #Ecto.Query<from l0 in Item, select: type(sum(l.price), l.price)>
  iex> Repo.all q
  [debug] QUERY OK source="items" db=6.1ms
  SELECT sum(l0."price")::money_with_currency FROM "items" AS l0 []
  [#Money<:USD, 600>]

The function Repo.aggregate/3 can also be used. However at least ecto version 3.2.4 is required for this to work correctly for custom ecto types such as :money_with_currency.

  iex> Repo.aggregate(Item, :sum, :price)
  #Money<:USD, 600>

Note that to preserve the integrity of Money it is not permissable to aggregate money that has different currencies. If you attempt to aggregate money with different currencies the query will abort and an exception will be raised:

  iex> Repo.all q
  [debug] QUERY ERROR source="items" db=4.5ms
  SELECT sum(l0."price")::money_with_currency FROM "items" AS l0 []
  ** (Postgrex.Error) ERROR 22033 (): Incompatible currency codes. Expected all currency codes to be USD

Aggregate functions: min() and max()

Money provides a migration generator which, when migrated to the database with mix ecto.migrate, supports performing min() and max() aggregation on Money types. The steps are:

  1. Generate the migration by executing mix money.gen.postgres.min_max_functions

  2. Migrate the database by executing mix ecto.migrate

  3. Formulate an Ecto query to use the aggregate function min() or max()

  # Formulate the query.  Note the required use of the type()
  # expression which is needed to inform Ecto of the return
  # type of the function
  iex> q = Ecto.Query.select Item, [l], type(min(l.price), l.price)
  #Ecto.Query<from l0 in Item, select: type(min(l.price), l.price)>
  iex> Repo.all q
  [debug] QUERY OK source="items" db=6.1ms
  SELECT min(l0."price")::money_with_currency FROM "items" AS l0 []
  [#Money<:USD, 600>]

The function Repo.aggregate/3 can also be used. However at least ecto version 3.2.4 is required for this to work correctly for custom ecto types such as :money_with_currency.

  iex> Repo.aggregate(Item, :min, :price)
  #Money<:USD, 600>

Note that to preserve the integrity of Money it is not permissable to aggregate money that has different currencies. If you attempt to aggregate money with different currencies the query will abort and an exception will be raised:

  iex> Repo.all q
  [debug] QUERY ERROR source="items" db=4.5ms
  SELECT min(l0."price")::money_with_currency FROM "items" AS l0 []
  ** (Postgrex.Error) ERROR 22033 (): Incompatible currency codes. Expected all currency codes to be USD

Order_by with Money

Since :money_with_currency is a composite type, the default order_by results may surprise since the ordering is based upon the type structure, not the money amount. Postgres defines a means to access the components of a composite type and therefore sorting can be done in a more predictable fashion. For example:

  # In this example we are decomposing the the composite column called
  # `price` and using the sub-field `amount` to perform the ordering.
  iex> q = from l in Item, select: l.price, order_by: fragment("amount(price)")
  #Ecto.Query<from l in Item, order_by: [asc: fragment("amount(price)")],
   select: l.amount>
  iex> Repo.all q
  [debug] QUERY OK source="items" db=2.0ms
  SELECT l0."price" FROM "items" AS l0 ORDER BY amount(price) []
  [#Money<:USD, 100.00000000>, #Money<:USD, 200.00000000>,
   #Money<:USD, 300.00000000>, #Money<:AUD, 300.00000000>]

Note that the results may still be unexpected. The example above shows the correct ascending ordering by amount(price) however the ordering is not currency code aware and therefore mixed currencies will return a largely meaningless order.

money_sql's People

Contributors

am-kantox avatar davidsulc avatar dhedlund avatar groguelon avatar kipcole9 avatar milangupta1 avatar morgz avatar namhoangyojee avatar redrabbit avatar seantanly avatar thbar avatar treere avatar zachdaniel avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

money_sql's Issues

Couldn't generate migration for custom type due to incorrect path

Hello,

First of all thanks for the package!

I have encountered an error when generating the migration using mix money.gen.postgres.migration. This is returned on my machine (Windows 10, Erlang/OTP 22, Elixir 1.8.2 compiled with Erlang/OTP 20):

> mix money.gen.postgres.migration
* creating priv/repo/migrations
** (File.Error) could not read file "c:/my_app/_build/dev/lib/ex_money/priv/SQL/postgres/create_money_with_currency.sql": no such file or directory
    (elixir) lib/file.ex:353: File.read!/1
    lib/mix/tasks/money_postgres_migration.ex:62: Mix.Tasks.Money.Gen.Postgres.Migration.migration_template/1
    lib/mix/tasks/money_postgres_migration.ex:39: anonymous fn/2 in Mix.Tasks.Money.Gen.Postgres.Migration.run/1
    (elixir) lib/enum.ex:769: Enum."-each/2-lists^foreach/1-0-"/2
    (elixir) lib/enum.ex:769: Enum.each/2
    (mix) lib/mix/task.ex:331: Mix.Task.run_task/3
    (mix) lib/mix/cli.ex:79: Mix.CLI.run_task/2
    (elixir) lib/code.ex:767: Code.require_file/2

I believe that the package could not find the file since it looked in ex_money instead of ex_money_sql, possibly following the package split, so I worked around it by copying priv/ from ex_money_sql to ex_money. I am very new to Elixir so I don't know how to fix it in code, but I hope this helps :)

Ecto types return malformed error when casting invalid currency

When casting an invalid currency, the both ecto types return an error tuple with another tuple as the second element.

{:error, {Cldr.UnknownCurrencyError, "The currency \"AAA\" is invalid"}}

This violates the typespec for Ecto.Type.cast: cast(term()) :: {:ok, term()} | {:error, keyword()} | :error, and causes an exception when used in Ecto.Changeset.cast

Please see this demo repo for a demonstration of the issue.

Incidentally, it appears that the load functions also violate the error typespec from Ecto.Type.

Unmatched clause for `Cldr.Decimal.parse/1`

Hi ๐Ÿ‘‹

Thanks for the great work on Money & CLDR!

I think there is a small bug in Money.Ecto.Composite.Type#cast/1: on L62 (https://github.com/kipcole9/money_sql/blob/master/lib/money/ecto/money_ecto_composite_type.ex#L62), the code calls Cldr.Decimal.parse/1 and the with clause matches on {amount, ""}, with else matching on {:error, {_, message}} or :error. However, when the amount cannot be parsed, Cldr.Decimal.parse/1 returns {:error, amount} (https://github.com/elixir-cldr/cldr_utils/blob/master/lib/cldr/decimal/decimal.ex#L34), which cannot match any of those.

iex(3)> Money.Ecto.Composite.Type.cast(%{"currency" => "USD", "amount" => "yes"})
** (WithClauseError) no with clause matching: {:error, "yes"}
    (ex_money_sql 1.3.1) lib/money/ecto/money_ecto_composite_type.ex:62: Money.Ecto.Composite.Type.cast/1

One option might be to change Money.Ecto.Composite.Type#cast/1 to match the correct return value, while the alternative could be to have Cldr.Decimal return the same :error atom that comes back from Decimal.parse/1 (https://github.com/ericmj/decimal/blob/v2.0.0/lib/decimal.ex#L1177) and which Money.Ecto.Composite.Type#cast/1 already expects to handle. I'll be happy to submit a PR in either case.

Sum returns ERROR 42804 (datatype_mismatch)

๐Ÿ‘‹๐Ÿผ Hi there!
I'm following the steps in the docs for aggregating money values, but after following the steps, I get the following error:

** (Postgrex.Error) ERROR 42804 (datatype_mismatch) returned record type does not match expected record type

Returned type character varying does not match expected type character(3) in column 1.
    (ecto_sql 3.10.2) lib/ecto/adapters/sql.ex:1047: Ecto.Adapters.SQL.raise_sql_call_error/1
    (ecto_sql 3.10.2) lib/ecto/adapters/sql.ex:945: Ecto.Adapters.SQL.execute/6
    (ecto 3.10.3) lib/ecto/repo/queryable.ex:229: Ecto.Repo.Queryable.execute/4
    (ecto 3.10.3) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
    (ecto 3.10.3) lib/ecto/repo/queryable.ex:151: Ecto.Repo.Queryable.one/3
    iex:5: (file)

Here's the migration for the schema part:

# ...

    create table(:products) do
      # ...
      add(:price, :money_with_currency, null: false)
    end

# ...

Here there query I'm using:

     query = from(p in Product, select: %{amount: type(sum(p.price), p.price)})

    Repo.one(query)

Which creates the following query:

SELECT sum(p0."price")::money_with_currency FROM "products" AS p0 []

Other than running the migration to create the specific sum functions for money_with_currency, is there anything else I might be missing? By the way, I'm using PostgreSQL 15.

Thanks in advance ๐Ÿ™Œ

Add Ecto Changeset validations

@DaTrader, @LostKobrakai, In commit cd2547d I have added Money.Validation.validate_money/3 that mirrors the way Ecto.Changeset.validate_number/3 operates.

Comments and suggestions welcome. Here is the draft changelog entry:

Enhancements

  • Adds the module Money.Validation to provide Ecto Changeset validations. In particular it adds Money.Validation.validate_money/3 which behaves exactly like Ecto.Changeset.validate_number/3 only for t:Money types.

Not compatible with Decimal 2.0

Hi there,

This library relies on Decimal.parse/1, which had a breaking change with the 2.0 release. Decimal is brought in as a transitive dependency from Money, which allows 2.0.0. This causes casting to crash if a project is updated to Decimal 2.0.

Money.Ecto.Map.Type.embed_as/2 should return :dump instead of :self

When using a Money.Ecto.Map.Type in an an embedded schema, the parameterized type is not loading the price correctly:

defmodule MyEmbed do
  embedded_schema do
    field(:price, Money.Ecto.Map.Type)
  end
end

Instead of loading the price through Money.Ecto.Map.Type, parsing the :amount with Decimal.parse/1, the field is loaded directly into a Money struct because the embed_as/2 callback implementation return :self.

While this appears to be ok when the locale is set to English, it is not the case and somehow dangerous with locales that use a comma instead of a dot for pricing format:

data = """
{
  "id": "431ab887-69a9-4fc6-85d4-3c0b83f8e2ed",
  "price": {
    "amount": "1.85",
    "currency": "CHF"
  }
}
"""

MyCldr.put_locale(:en)

Ecto.embedded_load(MyEmbed, Jason.decode!(data), :json)

returns CHF 1.85

%MyEmbed{
  id: "431ab887-69a9-4fc6-85d4-3c0b83f8e2ed",
  price: Money.new(:CHF, "1.85"),
}

but when changing the locale to German for example, things don't go as excepted:

MyCldr.put_locale(:de)

Ecto.embedded_load(MyEmbed, Jason.decode!(data), :json)

returns CHF 185

%MyEmbed{
  id: "431ab887-69a9-4fc6-85d4-3c0b83f8e2ed",
  price: Money.new(:CHF, "185"),
}

Changing the Money.Ecto.Map.Type.embed_as/2 to return :dump instead of :self fixes the issue and forces the data to be loaded through Money.Ecto.Map.Type.load/2.

Not sure if this is also the case with Money.Ecto.Composite.Type. But I assume this has the same issue and also need to return :dump instead of :self.

Aggregate function treating null as zero amount

@sergiotapia has expressed interest in aggregating NULL values as zero-amount money.

The current aggregate function is STRICT meaning that NULL values are not processed. In some cases it is desirable to return a zero-amount money rather than NULL.

Changing the current function to remove STRICT may have unexpected results and therefore a new function that explicitly has this behaviour may be required.

Dependencie issue with ex_money 5.2

Hello,

I'm using both ex_money and ex_money_sql.
I set the current latest version for both as following:

#mix.exs
defp deps do
  [
    {:ex_money, "~> 5.2"},
    {:ex_money_sql, "~> 1.3"},
    ..
   ]
end

But since this package (ex_money_sql) pull ex_money 5.0 I'm getting an unchecked dependency error:

Unchecked dependencies for environment dev:
* ex_money (Hex package)
  the dependency does not match the requirement "~> 5.2", got "5.0.2"
** (Mix) Can't continue due to errors on dependencies

Is it just a version bumping issue or is ex_money_sql not compatible with the latest ex_money?

I'm interested to install the latest version of ex_money (or at least 5.1) since I'm getting parse errors for string without currencies.
I see you talked about this issue and its update here: https://elixirforum.com/t/ex-money-money-with-currency-type/20143/8

NB: I'll try to see if I can fix it on my side and will come back if I managed to do it..

`undefined function migrations_path/1` when attempting to compile

I ran in to #1, but before seeing the issue I tried to clean my deps, refetch and recompile them.

Now, running mix deps.compile yields

== Compilation error in file lib/mix/tasks/money_gen_aggregate_functions.ex ==
** (CompileError) lib/mix/tasks/money_gen_aggregate_functions.ex:30: undefined function migrations_path/1
    (elixir) src/elixir_locals.erl:107: :elixir_locals."-ensure_no_undefined_local/3-lc$^0/1-0-"/2
    (elixir) src/elixir_locals.erl:107: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
could not compile dependency :ex_money_sql, "mix compile" failed. You can recompile this dependency with "mix deps.compile ex_money_sql", update it with "mix deps.update ex_money_sql" or clean it with "mix deps.clean ex_money_sql"

I'm able to compile when only ex_money is listed as a dependency, but not once I add ex_money_sql.

I'm using {:ecto_sql, "~> 3.0"}.

If I don't include ex_money_sql (so I can compile), and poke around in iex it appears that among the conditional clauses in https://github.com/kipcole9/money_sql/blob/master/lib/money/migration.ex have the following values: line 4 true, line 10 false, line 16 true.

Let me know if I can provide any other relevant info.

===

If I use the master branch as the dependency, I get the same error. But after commenting lines 4 and 8 in https://github.com/kipcole9/money_sql/blob/master/lib/money/migration.ex, I get the following when recompiling:

==> ecto
Compiling 54 files (.ex)
Generated ecto app
==> ex_cldr_currencies
Compiling 2 files (.ex)
Generated ex_cldr_currencies app
==> ex_cldr_numbers
Compiling 4 files (.erl)
Compiling 24 files (.ex)
Generated ex_cldr_numbers app
==> ex_money
Compiling 20 files (.ex)
warning: function Ecto.Migrator.migrations_path/1 is undefined (module Ecto.Migrator is not available)
  lib/money/migration.ex:5

Generated ex_money app
==> postgrex
Compiling 61 files (.ex)
Generated postgrex app
==> ecto_sql
Compiling 25 files (.ex)
Generated ecto_sql app
==> ex_money_sql
Compiling 7 files (.ex)
warning: redefining module Money.Migration (current version loaded from /home/david/Documents/dev/coinage_guru/ingestion_engine/_build/dev/lib/ex_money/ebin/Elixir.Money.Migration.beam)
  lib/money/migration.ex:1

Sooo, using the master branch of BOTH ex_money and ex_money_sql (and using override: true in ex_money to override the version specified as the ex_money_sql dependency), I then run in to issue #1 (so I guess it's not fixed on master?).

Then, I can apply the work around indicate there:

cp -r my_project_dir/_build/dev/lib/ex_money_sql/priv/SQL/postgres/* my_project_dir/_build/dev/lib/ex_money/priv/SQL/postgres/

And THEN the migration works. So it seems that once the migration looks for the files in the right place (or they are moved to where they're expected), bumping version numbers on both packages should solve the issues.

Composite type cast does not respect locales

iex(5)> Money.Ecto.Composite.Type.cast(%{currency: :NOK, amount: "218,75"})
** (WithClauseError) no with clause matching: {:error, "218,75"}
    (ex_money_sql 1.3.1) lib/money/ecto/money_ecto_composite_type.ex:62: Money.Ecto.Composite.Type.cast/1
iex(5)> Money.Ecto.Composite.Type.cast(%{currency: :NOK, amount: "218.75"})
{:ok, #Money<:NOK, 218.75>}
iex(6)> Money.new(:NOK, "218,72")
#Money<:NOK, 218.72>

The perpetrator is here:
https://github.com/kipcole9/money_sql/blob/master/lib/money/ecto/money_ecto_composite_type.ex#L52

The step trying to parse Decimal is unneccessary as Money does its own validation (which is also Locale aware).
So it will error out on invalid decimals (unknown strings or float source) and as such should not require this extra step with the with clause.

It's a pretty trivial change, do you just wanna plop the code change in and release new version or do you want me to send a PR?

Getting `type/0` is undefined errors when upgrading

I just went to upgrade a few dependencies:

ecto 3.6.1 => 3.6.2
ex_money 5.5.1 => 5.5.3
ex_money_sql 1.3.1 => 1.4.4

And all my tests are failing w/ this:

     ** (UndefinedFunctionError) function Money.Ecto.Composite.Type.type/0 is undefined or private. Did you mean one of:

           * type/1

     stacktrace:
       (ex_money_sql 1.4.4) Money.Ecto.Composite.Type.type()
       (ecto 3.6.2) lib/ecto/type.ex:906: Ecto.Type.adapter_dump/3
       (elixir 1.12.0) lib/enum.ex:2356: Enum."-reduce/3-lists^foldl/2-0-"/3
       (elixir 1.12.0) lib/enum.ex:1675: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
       (elixir 1.12.0) lib/enum.ex:1675: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
       (elixir 1.12.0) lib/enum.ex:2356: Enum."-reduce/3-lists^foldl/2-0-"/3
       (ecto 3.6.2) lib/ecto/repo/queryable.ex:208: Ecto.Repo.Queryable.execute/4
       (ecto 3.6.2) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3

I remember seeing you get into a conversation w/ Jose about this, upstream, in ecto, but I can't seem to find that. I don't see any issues filed here so I wanted to check if I'm missing something..

Thanks, as always, Kip!

Add keyword list to error return from money_with_currency cast/1

I have a problem with the parsing error messages being of variable and potentially sizeable length (the error messages are citing the input value). Also, I have a problem when the user starts typing the amount by first stating the currency so for as long as there is no number next to it, the error shows: Amount cannot be converted to a number: "".

I need to replace the former with a generic, fixed length message such as "Invalid amount", and the latter with no error message at all. As I now see it, the only way to do this is to intercept these in the LiveView generated MyApp.ErrorHelpers.error_tag/2 function, which is fine for me. But, the amount of information I get there is not enough. The details provided are the Money type tuple and the validation: :cast. as show below:

{"Amount cannot be converted to a number: """,
[type: {:parameterized, Money.Ecto.Composite.Type, []}, validation: :cast]}
Can you please also provide the error module atom itself so I can tell the errors one from another?

Cast function used to return error when currency is nil

Upgraded to version 1.6 and a condition is not handled in the money_sql code:

iex(1) > Money.Ecto.Composite.Type.cast(%{amount: "10", currency: nil})  
** (CondClauseError) no cond clause evaluated to a truthy value
    (digital_token 0.4.0) lib/digital_token.ex:142: DigitalToken.validate_token/1
    (ex_money 5.12.0) lib/money.ex:2280: Money.do_validate_currency/2
    (ex_money 5.12.0) lib/money.ex:211: Money.new/3
    (ex_money_sql 1.6.0) lib/money/ecto/money_ecto_composite_type.ex:96: Money.Ecto.Composite.Type.cast/2

Prior to 1.6, it was returning:

iex(1) > Money.Ecto.Composite.Type.cast(%{amount: "10", currency: nil})
{:error, [message: "The currency nil is invalid"]}

function Money.Ecto.Composite.Type.type/0 is undefined or private when using Ecto.Query.Api.type/2

Versions

Elixir: 1.11.3 (compiled with Erlang/OTP 23)
Ecto: 3.5.8
Erlang: 23.2.5
ex_money_sql: 1.4.3
ex_money: 5.5.1

The issue

I'm running into an issue where I am selecting a column and casting it to a composite money type with the type/2 function from Ecto.Query.API. Example:

MyModel
|> select([m], %{money: type(m.money, Money.Ecto.Composite.Type)})

And I get this error:

** (UndefinedFunctionError) function Money.Ecto.Composite.Type.type/0 is undefined or private. Did you mean one of:

      * type/1

The Ecto docs still specify type/0 as a valid callback so it's unclear to me why this was removed from Money.Ecto.Composite.Type. Any help would be greatly appreciated.

Thanks!

Broken/Unsupported CompositeType defaults

Having field defaults in a schema seems to be broken or unsupported.

  schema "my table" do
    field :value, Money.Ecto.Composite.Type, default: Money.new(:BRL, 0)
  end

Example:

     Assertion with == failed
     code:  assert bc_november.value == Money.new(:BRL, 0)
     left:  %Money{amount: #Decimal<0>, currency: :BRL, format_options: [{:default, #Money<:BRL, 0>}]}
     right: %Money{amount: #Decimal<0>, currency: :BRL, format_options: []}

Shouldn't we support https://hexdocs.pm/ecto/Ecto.Schema.html#field/3 options as well? default at least.
Mostly because even if the value isn't given, it defaults to something.

I'd more than happy to fix this. ๐Ÿ‘

Possible bug in ecto? Asking here first for clarification

Hi there,

I just tried upgrading to ex_money_sql ~> 1.4 and I have compilation errors in my application now. I think what's going on is that the Ecto type function from the Query API doesn't support parameterized types, but I'm not quite sure... If this is an issue with ecto itself, I can open an issue with them as well.

I've created a minimal repo to demonstrate the issue: https://github.com/doughsay/ecto_money
The details are in the readme, but I'll duplicate them here for convenience.

# insert some moneys:

%EctoMoney.Money{amount: Money.new("100.00", :USD)} |> EctoMoney.Repo.insert()
%EctoMoney.Money{amount: Money.new("50.00", :USD)} |> EctoMoney.Repo.insert()
%EctoMoney.Money{amount: Money.new("75.00", :USD)} |> EctoMoney.Repo.insert()

# run a custom sum query and cast the response to a Money struct:

import Ecto.Query

query =
  from(
    money in "moneys",
    select: %{
      total: type(sum(money.amount), EctoMoneyType),
    }
  )

EctoMoney.Repo.all(query)

Expected:

This is the response you get when using ex_money_sql == 1.3.1

[%{total: #Money<:USD, 225.00>}]

Actual:

Using ex_money_sql ~> 1.4, with the new parameterized type, we get a
compilation error:

** (UndefinedFunctionError) function Money.Ecto.Composite.Type.type/0 is undefined or private. Did you mean one of:

      * type/1

    (ex_money_sql 1.4.2) Money.Ecto.Composite.Type.type()
    (ecto 3.5.7) lib/ecto/type.ex:421: Ecto.Type.match?/2
    (ecto 3.5.7) lib/ecto/query/builder.ex:646: Ecto.Query.Builder.assert_type!/3
    (ecto 3.5.7) lib/ecto/query/builder.ex:415: Ecto.Query.Builder.escape/5
    (ecto 3.5.7) lib/ecto/query/builder.ex:462: Ecto.Query.Builder.escape_with_type/5
    (ecto 3.5.7) lib/ecto/query/builder/select.ex:111: anonymous fn/4 in Ecto.Query.Builder.Select.escape_pairs/4
    (elixir 1.11.3) lib/enum.ex:1533: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
    (ecto 3.5.7) lib/ecto/query/builder/select.ex:82: Ecto.Query.Builder.Select.escape/4

Workarounds

You can remove the casting, but then you get back a tuple instead of a Money
struct.

query =
  from(
    money in "moneys",
    select: %{
      total: sum(money.amount),
    }
  )

EctoMoney.Repo.all(query)

Results in:

[%{total: {"USD", #Decimal<225.00>}}]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.