# Money operations

The `money` operations make, take apart, add up, share out and convert
amounts of [`Money`](money-and-payments.md). Adding, subtracting and
comparing amounts use the ordinary operators; the operations below do
the rest. The "Fails" column names the failure a call can give, which
you map in an `attempt` (each mapping line is an arm,
[Attempt](attempt.md)).

| Operation | Gives | Fails | SQL form |
|---|---|---|---|
| `money.from_minor(amount, currency)` | `Money` of `amount` minor units | | none |
| `money.of(amount, currency)` | `Money` from a `Decimal` in major units | `too_many_places` (more places than the currency has) | none |
| `money.minor(value)` | `Int`, the minor units | | the amount column |
| `money.currency(value)` | `Currency` | | the currency column |
| `money.sum(items, currency:)` | `Money`; `currency:` gives the zero for an empty list | `currency_mismatch` | none |
| `money.allocate(total, ratios)` | `List<Money>` in proportion to `ratios` (whole numbers), adding up to `total` exactly: leftover minor units go one each to the first shares | | none |
| `money.split(total, parts)` | `List<Money>`, `parts` near-equal shares adding up to `total`; `parts` at least 1 | | none |
| `money.mask(card)` | `MaskedCardNumber` of a `PaymentCardNumber`: its brand by its issuer range (`none` for a range no listed scheme issues) and its last four | | none |
| `money.convert(value, rate)` | `Money` in the rate's other currency, rounded half to even to its minor unit; `rate` an `ExchangeRate` the app was given | `currency_mismatch` (the rate is for another currency) | none |
| `money.apply_discount(price, discount)` | `Price`, `discount` taken off its amount and `includes_tax` kept: an amount off, or that percentage of the amount rounded half to even to a minor unit; more off than the price leaves nothing | `currency_mismatch` (an amount off in another currency) | none |

## Money arithmetic

`+`, `-` and comparisons work within one currency. Mixing two currencies
is a compile error where both types fix their currency. Where the
currencies are known only while the program runs, it is the failure
`currency_mismatch`, which you handle in an `attempt`.

You can multiply money by a number or a `Percentage`, and divide it by a
`Decimal`. The result is rounded by the type's `rounding:`, which is
`half_even` unless the type declares another. Dividing money by an `Int`
is refused: use `money.split` instead. The full rules are in
[The standard operator table](the-standard-operator-table.md).

## Sharing money out

`money.split` and `money.allocate` never round: every minor unit goes to
some share, so the shares always add up to the total.

- `money.split`'s `parts` is never 0 and never negative. The compiler
  proves it, as it proves a divisor or a count.
- `money.allocate`'s ratios are a list of whole numbers. The list is
  never empty (`type.argument_empty`) and no ratio is negative
  (`type.argument_negative`). Ratios that are all 0 are a defect, as
  dividing by zero would be.
- Leftover minor units go one each to the first shares whose ratio isn't
  0, so a share whose ratio is 0 always gets nothing.
- A negative total, such as a refund, shares out as a payment does, and
  every share is negative.

## Calls that need no arm

In some cases a call can't fail as its "Fails" column says, and its
`attempt` needs no arm for that failure:

- `money.sum` checks every item's currency against `currency:`, which is
  also the total of an empty list. When the items' type fixes that
  currency, the call needs no arm.
- `money.of`'s `too_many_places` needs no arm when the currency is
  written out and the amount is an `Int`, a literal with no more decimal
  places than the currency has, or a type whose `scale` is no more than
  the currency's exponent (its number of decimal places).

## Converting between currencies

`money.convert` applies an `ExchangeRate` your app already holds, so a
rate applied the wrong way round is a type error, not a silent bug.
Jadpo doesn't fetch rates: a rate is outside data with a time and a
source, which a provider package fetches
([Exchange rates](exchange-rates.md)).

The result is rounded half to even to the target currency's minor unit,
whatever the amount type's `rounding:` says: £10.50 at 1.17 is €12.285,
so €12.28.

## Money as text

Money's canonical text is the currency code, a space, and the amount in
major units with exactly the currency's number of decimal places:
`GBP 10.50`, `JPY 500`, `BHD 1.250`. `text.parse` reads the code in any
case. It refuses any other number of places, or an amount past
[`Money`'s limit](money-and-payments.md), as `malformed`.

Money has no literal, so `Display`, the form test reports and failed
assertions use, writes an amount as the code that makes one:
`money.from_minor(1050, Currency.gbp)`.

## Storing money

A money field is stored in two columns: a `bigint` of minor units named
`<column>_amount`, and a `char(3)` named `<column>_currency` holding the
ISO 4217 code. It uses both even when the type fixes the currency, so
opening the type to other currencies later needs no migration, and a
query can group by currency.

A type that fixes the currency also adds a check constraint,
`<table>_<column>_currency_check`, holding the currency column to that
code. A migration adds or drops it as the type changes.

## Money in JSON

Money is sent in one fixed shape, the amount in integer minor units as
Stripe sends it: `{"amount": 1050, "currency": "GBP"}`. `json.read` and
`json.write` use the same shape.

- The amount is always a JSON number. It is never sent as a string,
  which is how [Values and types](values-and-types.md) sends a large
  `Int`.
- JSON past `Money`'s limit is refused as `max`.
- The currency is read in any case and written in capitals.
- OpenAPI describes it by the JSON it sends, as it does every standard
  type: an object of `amount`, with the type's bounds, and `currency`, an
  `enum` of the one code when the type fixes it.

A provider that uses another shape, such as `"10.50"`, is mapped in its
package's `wire:`.

## Showing money to people

`text.format` shows money the way a locale writes it, from CLDR's data:
the symbol or code, the locale's grouping and decimal separators, the
currency's decimal places and where the symbol goes. For example
`£1,234.50`, or `1 234,50 €`. It takes three options, each a built-in
enum:

- `precision:` is a `MoneyPrecision`. The default, `MoneyPrecision.minor`,
  gives the currency's places as CLDR gives them for display.
  `MoneyPrecision.whole` gives `£1,235`, rounding a half away from zero.
- `display:` is a `MoneyDisplay`: `symbol` (the default), `code` or
  `name`.
- `sign:` is a `MoneySign`: `standard` or `accounting`, which shows
  `(£5.00)`.

## Money in a translated message

Inside a [translated message](translations-as-messages.md),
a money field is written `{price, number, currency}`, one of the
messages' placeholder styles. It shows the amount as `text.format` does
with its default options, in the locale the message is written in. Code
uses `text.format`; messages use the placeholder.

It is the only style for a field built on `Money`, in a message's
wording or an email's subject, and it is written exactly so:

- `{price}` for an amount, or this style on any other field, is
  `translation.placeholder_style`, whose edit writes the right one.
- Any other spelling, such as `{price,number,currency}`, is
  `translation.wording_invalid`.

A failure's `client:` fields can't be money: the client app formats
those itself, and the export has no money format yet.

An order keeps its total, in pounds, and a deposit in the customer's
currency, each in two columns:

```jadpo
use bakery/types { CakePrice, Deposit }

/// A cake ordered for collection.
entity Order {
    id: Uuid
    total: CakePrice  // columns `total_amount` and `total_currency`
    deposit: Deposit?
}
```

```jadpo
use bakery/entities { Order }
use bakery/types { CakePrice, Deposit }
use bakery/actions { pay_deposit }

test "an order keeps its amounts as they were written" {
    var placed: Order = place_order(CakePrice(money.from_minor(4250, Currency.gbp)))
    var order_on_file: Order = attempt Order.get(placed.id)
    assert order_on_file.total == money.from_minor(4250, Currency.gbp)
    assert order_on_file.deposit == none
    var paid: Order = pay_deposit(order_on_file, Deposit(money.from_minor(2000, Currency.eur)))
    var order_after_deposit: Order = attempt Order.get(placed.id)
    assert order_after_deposit.deposit == paid.deposit
    assert order_after_deposit.deposit == Deposit(money.from_minor(2000, Currency.eur))
}
```

Not built yet: a query that compares or orders by a money amount, a
one-step update that works one out, and a backfill of a new required
money field. Each is reported as `toolchain.not_built_yet`.
