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.
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 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.
better-supabase sql add quotesThe module needs tenant, access and subjects.
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
| 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) 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 customer's page calls two functions that run without signing in:
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 module or your edge.
decide(quoteId, decision, { by, note }) records an answer the customer gave another way, such as by phone.
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 module installed, it links the quote to the deal with created_from, so lineage shows deal, quote, invoice.
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 module installed, the quote's owner gets a notification when the customer answers.
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
| 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 |
Last updated on
Work items
Issues and tasks on pipelines, with identifiers, parents without cycles, several assignees, priority 0 to 4, estimates, start, end and due dates, locking, projects, iterations, batched edits and a progress tree.
Invoices
Invoices an organization sends its own customers, with drafts and lines, a gapless number on issue, payments and refunds, credit notes, void and write-off, overdue and aging per currency, and invoices from accepted quotes.