# Prices, discounts and rates

These types hold a price, a discount, a price per kilogram or litre, and
the rates a business quotes: tax, basis points and interest. Each is
built from `Money`, `Percentage` or a [quantity](physical-quantities.md):

| Type | What it holds | Sent as | Shown as |
|---|---|---|---|
| `Price` | `amount`, a `Money` never below 0, and `includes_tax`, a `Bool`, so a gross price is never taken for a net one | `{"amount": {"amount": 1050, "currency": "GBP"}, "includes_tax": true}` | Its amount, `£10.50` |
| `Discount` | A choice: `amount_off { amount }`, a `Money` never below 0, or `percentage_off { percentage }`, from 0% to 100% | `{"tag": "percentage_off", "percentage": "20%"}` | Its part, `£5.00` or `20%` |
| `UnitPrice` | A choice by the quantity's kind, as the UK's Price Marking Order prices goods: `per_mass`, `per_volume`, `per_length` or `per_area`, each a `price` and the amount it is `per` | `{"tag": "per_mass", "price": {...}, "per": {"value": "1", "unit": "kg"}}` | `£2.50/1 kg` |
| `TaxRate` | A `Percentage` never below 0% | `"20%"` | `20%` |
| `BasisPoints` | A whole number of hundredths of a percent, of any sign | `25`, a JSON number | `25 bp` |
| `InterestRate` | `rate`, a `Percentage` a year that may be below 0%, and `basis`, an `InterestRateBasis`: `aer`, `apr` or `flat` | `{"rate": "5.5%", "basis": "apr"}` | `5.5% APR` |

## Choices: `Discount` and `UnitPrice`

`Discount` and `UnitPrice` are each a choice between variants: an
[enum with data](enums.md), the language's tagged
union, where each variant carries its own fields. You build, match and
read one as you would your own enum.
`Discount.percentage_off { percentage: 20% }` builds one, and the
`match` arm `Discount.percentage_off(off) -> off.percentage` reads it.
It is sent in JSON in the same tagged form as your own enums, and shown
by the parts its variant has.

The four records, `Price`, `Discount`, `UnitPrice` and `InterestRate`,
have no single text form, so `text.from` and `text.parse` refuse them, as
they refuse a `PostalAddress`. None is ordered, and `.all` refuses a
choice, as it refuses any enum with data.

## `TaxRate` and the parts' bounds

A `TaxRate` is a `Percentage` that is never below 0%. The standard
library declares it as a type built on `Percentage` with `min: 0%`, as it
declares the types held as text, so building one below 0% breaks `min`.
Its text, JSON, storage and format are `Percentage`'s. It has no maximum,
because some duties are set above 100%.

The records' parts keep bounds the same way: a price's `Money` and an
amount off are never below 0, a percentage off is from 0% to 100%, and a
unit price's amount is never below 0.

## `Price`

A `Price` is an `amount` and `includes_tax`, so a price with tax is
never taken for one without. It is shown as its amount, because CLDR has
no words for tax status; your app writes "inc. VAT" itself.

## `Discount`'s variants

`Discount`'s variants, `amount_off` and `percentage_off`, each hold one
named field, because a variant's data is always named fields.

## Applying a discount

`money.apply_discount(price, discount)` takes a `Price` and gives one,
keeping `includes_tax`. It is an operation in `money` because
[the standard operator table](the-standard-operator-table.md) has no row
for a `Discount`.

A percentage's share is rounded half to even to a minor unit before it
comes off, so a receipt's discount line and its total agree. More off
than the price leaves nothing, never less. An amount off in another
currency fails `currency_mismatch`. Which variant a `Discount` holds is
known only while the program runs, so every call needs its `attempt`.

## `UnitPrice`

A `UnitPrice` is a price per amount of something, as a shelf label
shows it. It is a choice by the quantity's kind, `per_mass`,
`per_volume`, `per_length` or `per_area`, the kinds UK law prices goods
by. Each holds a `price` and the amount it is `per` (`1kg`, `100g`).

It is shown as the price over the amount in its kind's base unit,
`£2.50/1 kg`, by CLDR's short "per" pattern. A release adds a kind, such
as energy per kWh, when an application needs one.

## `BasisPoints`

A `BasisPoints` is a whole number of hundredths of a percent, of any
sign, held within JavaScript's safe integers as an amount of money is.
Its text is its digits, `-25` for a negative one. It is sent as a JSON
number, and shown with `bp` in every locale: `25 bp`. There is no
conversion to a `Percentage`.

## `InterestRate`

An `InterestRate` is a `rate` a year, which may be below 0%, and a
`basis`. The basis is an `InterestRateBasis`, a standard enum of `aer`,
`apr` and `flat`. Each value is its own code, and is shown as lenders
write it, in every locale: `AER`, `APR`, `flat`.

None of these types has a shape, the set of fields Jadpo uses to spot an
app type that copies a standard one (`type.standard_exists`). None is
personal data.

A bakery puts offers on its cakes, works out the VAT on a price before tax, and
labels its apples and its loan as the law and the lender write them:

```jadpo
type WrongCurrency = Failure {
    kind: FailureKind.invalid_value
}

type ShareOff = Percentage {}

type CakeAmount = Money {}

type FormText = Text {}

type NotARate = Failure {
    kind: FailureKind.invalid_value
}

/// A cake's price with a deal taken off it; an amount off in another
/// currency fails.
function with_deal(price: Price, deal: Discount) fails WrongCurrency -> Price {
    var reduced: Price = attempt money.apply_discount(price, deal) {
        currency_mismatch: WrongCurrency
    }
    return reduced
}

/// A deal as the shop's window shows it.
function deal_label(deal: Discount, customer: Locale) -> PresentationText {
    return text.format(deal, locale: customer)
}

/// The share of the price a deal takes off, where it names one.
function share_off(deal: Discount) -> ShareOff? {
    return match deal {
        Discount.amount_off -> none
        Discount.percentage_off(off) -> ShareOff(off.percentage)
    }
}

/// The VAT on a price before tax at `rate`, rounded half to even to a
/// penny.
function vat_on(untaxed: Price, rate: TaxRate) -> CakeAmount {
    return CakeAmount(untaxed.amount * rate)
}

/// A VAT rate typed into the till's settings.
function vat_rate_from(raw: FormText) fails NotARate -> TaxRate {
    var rate: TaxRate = attempt text.parse(raw, as: TaxRate) {
        malformed: NotARate
        invalid: NotARate
    }
    return rate
}

/// Apples, priced by the kilogram as the shelf label must show them.
function apples() -> UnitPrice {
    return UnitPrice.per_mass {
        price: money.from_minor(250, Currency.gbp)
        per: 1kg
    }
}

/// The rate on the bakery's loan, as the lender quotes it.
function loan_rate() -> InterestRate {
    return InterestRate {
        rate: 6.9%
        basis: InterestRateBasis.apr
    }
}
```

```jadpo
test "a deal comes off a price either way" {
    var cake: Price = Price {
        amount: money.from_minor(1250, Currency.gbp)
        includes_tax: true
    }
    var five_off: Discount = Discount.amount_off {
        amount: money.from_minor(500, Currency.gbp)
    }
    var fifth_off: Discount = Discount.percentage_off {
        percentage: 20%
    }
    var after_five: Price = attempt with_deal(cake, five_off)
    assert after_five.amount == money.from_minor(750, Currency.gbp)
    assert after_five.includes_tax
    var after_fifth: Price = attempt with_deal(cake, fifth_off)
    assert after_fifth.amount == money.from_minor(1000, Currency.gbp)
    var in_euros: Discount = Discount.amount_off {
        amount: money.from_minor(500, Currency.eur)
    }
    assert with_deal(cake, in_euros) fails WrongCurrency
    assert deal_label(five_off, Locale.en_gb) == "£5.00"
    assert deal_label(fifth_off, Locale.en_gb) == "20%"
    assert share_off(fifth_off) == 20%
    assert share_off(five_off) == none
}

test "more off than the price leaves nothing" {
    var bun: Price = Price {
        amount: money.from_minor(300, Currency.gbp)
        includes_tax: true
    }
    var five_off: Discount = Discount.amount_off {
        amount: money.from_minor(500, Currency.gbp)
    }
    var after: Price = attempt with_deal(bun, five_off)
    assert after.amount == money.from_minor(0, Currency.gbp)
}

test "rates are shown as they are quoted" {
    var untaxed: Price = Price {
        amount: money.from_minor(1000, Currency.gbp)
        includes_tax: false
    }
    var vat: TaxRate = attempt vat_rate_from("20%")
    assert vat_on(untaxed, vat) == money.from_minor(200, Currency.gbp)
    assert vat_rate_from("-5%") fails NotARate
    assert vat_rate_from("20") fails NotARate
    assert text.format(apples(), locale: Locale.en_gb) == "£2.50/1 kg"
    assert text.format(loan_rate(), locale: Locale.en_gb) == "6.9% APR"
    var over_base: BasisPoints = 25
    assert text.format(over_base, locale: Locale.en_gb) == "25 bp"
    assert text.from(over_base) == "25"
}
```

A percentage off past 100% doesn't compile, as a literal past any type's
bounds doesn't:

```jadpo
/// Everything off, and more.
function clearance() -> Discount {
    return Discount.percentage_off {
        percentage: 120%
    }
}
```

```text
error: `120%` isn't a valid `Discount.percentage_off.percentage`
 --> main.jadpo:4:21
  |
4 |         percentage: 120%
  |                     ^^^^ breaks `max: 100%`
  reason: `Discount.percentage_off.percentage` declares `max: 100%`, and the value is above 100%. A literal is checked against every rule while compiling.
  next: Use a value that `Discount.percentage_off.percentage` allows (agent)
  rule: type.invalid_literal
  help: https://jadpo.dev/docs/errors/type.invalid_literal

check failed: 1 error, 1 file
```

## Storing prices and rates

A `TaxRate` is stored in one `numeric` column and a `BasisPoints` in one
`bigint`. The four records are stored a column per part, as an
`ExchangeRate` is. A `Price` uses `amount`, `currency` and
`includes_tax`. A choice such as `Discount` stores its variant's name in
`kind`, then a column for each part its variants hold, `NULL` where the
stored variant doesn't have that part:

```jadpo
/// A loan the bakery took out, at a rate above the base rate.
entity Loan {
    id: Uuid
    over_base: BasisPoints  // one `bigint` column: 25 for 0.25%
    vat_rate: TaxRate  // one `numeric` column: 0.2 for 20%
}
```

```jadpo
use bakery/entities { Loan }

test "a loan keeps its rate over base as it was written" {
    var taken: Loan = take_loan(25, 20%)
    var on_file: Loan = attempt Loan.get(taken.id)
    assert on_file.over_base == 25
    assert on_file.vat_rate == 20%
}
```
