Current section
Files
Jump to
Current section
Files
README.md
# PTAX
An Elixir library for currency conversion at Brazil's official exchange rate. `:ptax` converts `Money` amounts between any two currencies quoted by the Brazilian Central Bank (Banco Central do Brasil, BCB), for the latest business day or for any past date, matching BCB's own [online converter](https://www.bcb.gov.br/conversao).
The library takes its name from those rates. PTAX is the reference rate used in financial contracts, tax reporting, and regulatory filings in Brazil: on every business day BCB publishes a bulletin on its [exchange rates page](https://www.bcb.gov.br/estabilidadefinanceira/cotacoestodas), listing, per currency, a bid and an ask against BRL and against USD.
## Installation
Add `:ptax` to your project's dependencies in `mix.exs`:
```elixir
# mix.exs
def deps do
[
{:ptax, "~> 3.0"}
]
end
```
## Usage
Converted amounts are rounded to 7 decimal places. The rate itself is never rounded, so scaling the amount scales the result exactly.
### Convert using the latest published quotes
```elixir
iex> PTAX.exchange(Money.new!(:USD, "100"), :BRL)
{:ok, %Money{}}
iex> PTAX.exchange!(Money.new!(:USD, "100"), :BRL)
%Money{}
```
The bulletin comes out at 13:00 in São Paulo. Before that time the lookup reads the previous business day's bulletin, and it falls back one more business day when the expected bulletin is not available yet. Business days skip weekends and the Brazilian national bank holidays, the same calendar that [ANBIMA](https://www.anbima.com.br/feriados/feriados.asp) publishes.
### Convert using the quotes for a specific date
```elixir
iex> PTAX.exchange(Money.new!(:GBP, "50"), :BRL, ~D[2026-07-31])
{:ok, Money.new(:BRL, "341.8150000")}
iex> PTAX.exchange!(Money.new!(:GBP, "50"), :BRL, ~D[2026-07-31])
Money.new(:BRL, "341.8150000")
```
Dates with no bulletin (weekends, holidays) return an error, or raise with the bang variants:
```elixir
iex> PTAX.exchange(Money.new!(:USD, "100"), :BRL, ~D[2025-12-25])
{:error, %PTAX.QuotesNotFoundError{date: ~D[2025-12-25]}}
iex> PTAX.exchange!(Money.new!(:USD, "100"), :BRL, ~D[2025-12-25])
** (PTAX.QuotesNotFoundError) no quotes published for 2025-12-25
```
Currencies BCB does not quote return a `PTAX.CurrencyNotQuotedError`:
```elixir
iex> PTAX.exchange(Money.new!(:USD, "100"), :ZWL, ~D[2026-07-31])
{:error, %PTAX.CurrencyNotQuotedError{currency: :ZWL}}
```
> #### Always convert in a single call {: .warning}
>
> BCB treats a conversion between two currencies other than BRL and USD as its own operation, not as a conversion into USD followed by one out of it. Routing an amount through an intermediate currency yourself does not reproduce the published result, and the difference reaches several percent on currencies with a wide spread.
Once BCB publishes a bulletin it never changes, so each one is downloaded once and cached on disk. A bulletin that cannot be written or read is fetched from BCB instead.
## Command line
`:ptax` also ships as an escript. Install it with [`mix escript.install`](https://hexdocs.pm/mix/Mix.Tasks.Escript.Install.html):
```console
$ mix escript.install hex ptax
```
Pass the amount with its currency, as a code or a symbol. The result is in BRL unless `--to` names another currency, and `--date` picks the bulletin to use:
```console
$ ptax 50 GBP --date 2026-07-31
R$ 341,82
$ ptax "US$ 100"
R$ 507,67
$ ptax 100 BRL --to USD
US$ 19,70
```
## Testing
`PTAX` reads quotes through the `PTAX.Rates` behaviour. Point `:ptax, :rates` at a stub implementing it and a test suite serves known rates instead of reaching BCB.
Any module implementing the behaviour works. With [Mox](https://hexdocs.pm/mox):
```elixir
# mix.exs
{:mox, "~> 1.2", only: :test}
# config/test.exs
config :ptax, rates: MyApp.RatesMock
# test/test_helper.exs
Mox.defmock(MyApp.RatesMock, for: PTAX.Rates)
ExUnit.start()
```
Both callbacks receive the pair being converted and return the rate from the first currency to the second, as a `Decimal`: `rate/3` for the given date, used by `PTAX.exchange/3`, and `rate/2` for the latest bulletin, used by `PTAX.exchange/2`.
```elixir
defmodule MyApp.ConversionTest do
use ExUnit.Case, async: true
test "converts at the published rate" do
Mox.stub(MyApp.RatesMock, :rate, fn
:USD, :BRL, ~D[2026-07-31] -> {:ok, Decimal.new("5.4321")}
:BRL, :USD, ~D[2026-07-31] -> {:ok, Decimal.new("0.1834")}
end)
assert PTAX.exchange!(Money.new!(:USD, "100"), :BRL, ~D[2026-07-31]) == Money.new(:BRL, "543.2100000")
assert PTAX.exchange!(Money.new!(:BRL, "100"), :USD, ~D[2026-07-31]) == Money.new(:USD, "18.3400000")
end
end
```
## See also
- [`ex_money`](https://hexdocs.pm/ex_money) — the `Money` type PTAX converts