# Quotes

> Quotes with line items and totals, a revision chain, a public link the customer accepts, rejects or asks changes through, expiry, and quotes made from deals.

Source: https://bettersupabase.com/docs/blocks/quotes

`quotes` is the offer you send a customer: a title, a company and a contact, lines with quantities, unit amounts, discounts and tax in minor units, totals the database keeps, a valid-until date and terms. You send it as a link the customer opens without an account, and they accept it, reject it or ask for changes. A new version is a revision with the same number. With the [deals](/docs/blocks/deals) module installed, a quote starts from a deal.

Reading needs `quotes.read`, and creating, sending and recording decisions needs `quotes.write`. Admins get every permission, and members get `quotes.read` and `quotes.write`.

```bash
better-supabase sql add quotes
```

The module needs `tenant`, `access` and `subjects`.

```ts
import { createQuotes } from "better-supabase/blocks/quotes";

const quotes = createQuotes({ transport });
const quote = await quotes.fromDeal(dealId).orThrow();
await quotes
  .setLines(quote.id, [
    {
      description: "Licences",
      quantity: 10,
      unitAmount: 100_000n,
      taxRateBps: 2100,
    },
    {
      description: "Setup",
      unitAmount: 50_000n,
      discountBps: 2000,
      taxRateBps: 2100,
    },
  ])
  .orThrow();
const { token } = await quotes.send(quote.id).orThrow();
// Email the customer a link such as https://app.example.com/q/{token}
```

## Lifecycle [#lifecycle]

| Status                 | How it gets there                                                           |
| ---------------------- | --------------------------------------------------------------------------- |
| `draft`                | `create` or `fromDeal`; lines and fields can change                         |
| `sent`                 | `send`; the quote freezes and gets its link                                 |
| `viewed`               | the customer opened the link                                                |
| `accepted`, `rejected` | the customer answered through the link, or `decide` recorded their answer   |
| `changes_requested`    | the customer asked for changes; it can be edited and sent again, or revised |
| `expired`              | its valid-until date passed (`expire_quotes`, or an answer after the date)  |
| `withdrawn`            | a newer revision replaced it                                                |

`create` gives a quote the next number (`Q-12`, or the format of a `quote` entity in [number-sequences](/docs/blocks/number-sequences)) and a valid-until date `options.validDays` (default 30) from today. Its company and contact are records in `config.subjects` (the parties types `company` and `person` by default), checked like a deal's (`QUOTE_COMPANY_FORBIDDEN`).

`send` needs at least one line (`QUOTE_EMPTY`). It returns the link's token once; only its hash is stored. `revise(quoteId)` makes a new draft with the same number, the next revision, the same lines and a fresh valid-until date, and withdraws the old one unless it was accepted, which also ends its link.

## The customer's page [#the-customers-page]

The customer's page calls two functions that run without signing in:

```ts
const quote = await quotes.view(token).orThrow(); // marks it viewed
await quotes.respond(token, "accepted", { name: "Els Jansen" }).orThrow();
```

`view` returns the quote without internal notes: its identifier, title, status, currency, valid-until date, terms, totals and lines. `respond` takes `accepted`, `rejected` or `changes_requested` with the customer's name and a note; after the valid-until date it expires the quote instead. A withdrawn or unknown link answers `QUOTE_LINK_INVALID`. Rate-limit the page with the [rate-limit](/docs/blocks/sql) module or your edge.

`decide(quoteId, decision, { by, note })` records an answer the customer gave another way, such as by phone.

## From a deal [#from-a-deal]

`fromDeal(dealId)` makes a draft from the deal's name, company, primary contact, currency, owner and lines, or one line for its amount when it has none. It returns the same quote when called again, and with the [relations](/docs/blocks/relations) module installed, it links the quote to the deal with `created_from`, so `lineage` shows deal, quote, invoice.

## Events [#events]

| Event                                                                           | Data                                                                  |
| ------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `quote.created`, `quote.updated`, `quote.sent`, `quote.viewed`, `quote.expired` | `organizationId`, `quoteId`, `subjectType`, `subjectId`, `identifier` |
| `quote.accepted`                                                                | the same, with `total`, `currency` and `byName`                       |
| `quote.rejected`, `quote.changes_requested`                                     | the same, with the customer's `note`                                  |
| `quote.revised`                                                                 | the same, with `revisionId`                                           |

With the [notifications](/docs/blocks/notifications) module installed, the quote's owner gets a notification when the customer answers.

## Hooks [#hooks]

The module calls `before_quote_send(quote uuid)`, `after_quote_send(quote uuid)`, `before_quote_accept(quote uuid)` and `after_quote_accept(quote uuid)`, so an app can check margins before a quote goes out or start an order when it is accepted.

## Errors [#errors]

| Hint                     | Cause                                                |
| ------------------------ | ---------------------------------------------------- |
| `QUOTE_FORBIDDEN`        | the caller lacks `quotes.write`                      |
| `QUOTE_ATTRS_INVALID`    | a missing title or a field with a bad value          |
| `QUOTE_NOT_FOUND`        | no quote the caller can edit                         |
| `QUOTE_NOT_EDITABLE`     | the quote was sent; revise it                        |
| `QUOTE_EMPTY`            | a quote without lines can't be sent                  |
| `QUOTE_NOT_OPEN`         | the quote doesn't take an answer now                 |
| `QUOTE_NOT_SENT`         | a draft is edited, not revised                       |
| `QUOTE_ALREADY_REVISED`  | the quote already has a newer revision               |
| `QUOTE_DECISION_INVALID` | answer `accepted`, `rejected` or `changes_requested` |
| `QUOTE_LINK_INVALID`     | the link is unknown or was withdrawn                 |
| `QUOTE_NAME_REQUIRED`    | the customer's answer needs a name                   |
| `QUOTE_ORIGIN_NOT_FOUND` | no deal the caller can quote                         |