brick-quote
Quotes a company sends its customers: versioned priced offers, a discount policy with deal-desk approval, and the customer's answer. One brick on erp-platform, the first rebuilt from a general CRM model. Every action is both an API route and an MCP tool, so a rep's agent in Claude or ChatGPT can do exactly what the rep can, and no more.
What it does
| Story (CRM spec) | How |
|---|---|
| CRM-05 build a quote from the price book | add_line prices each product from the price book in the quote's currency as of a date; totals, list totals and the effective discount are computed, never typed |
| CRM-06 discounts above the threshold go to the deal desk | the discount policy (a row) has an auto-approve level and a maximum, applied to the deepest line's discount as well as the net (0.2.0), whichever is larger: a price above list is allowed but never offsets another line's discount. Between them a send is refused 403 with the figures until the desk approves that percentage; above the maximum the line is refused at entry. Nobody approves their own quote. A discount is never shown below 0.00 |
| CRM-07 a sent quote is locked | a sent version refuses every edit; revise_quote makes a new draft version, and the sent one stays as the customer read it |
Plus: accepting supersedes other open versions (accepting twice is one acceptance); an expired quote cannot be accepted; each line's revenue schedule (months it is recognised in) is stored and sums back to the line; every change lands in the quote's history with who and when.
0.2.0 — the CRM audit's wave 1 (2026-10-17)
| Finding | Now |
|---|---|
| C1 a quote could be attached to any deal — another rep's, another customer's — and then win it | open_quote asks the pipeline for the deal as the caller: 404 if they may not see it, 409 if it is for another customer, in another currency, or closed. quote-facts names each accepted quote's customer (accepted: [{quote_id, bp_id, currency, total, version}]), so the pipeline counts only the deal's own customer's |
| C3 eight lines added at once kept one | a quote carries a revision (its optimistic version — version is the offer the customer reads). GET sends it as the ETag; every change takes revision or If-Match and answers 409 "changed since you opened it" when stale; without one, a lost race is re-run on the latest — eight concurrent adds are eight lines (verified on DataK3) |
| C4 | bp_id leaves as a string of digits |
| H2 a +80% line hid a 40% discount | the deepest line is read with the net (above) |
| H4 a second open for a deal made another quote silently | the quote in flight comes back with existing: true (add_lead's style); one the caller may not see is 409; two opens at once make ONE quote (a plain-INSERT claim of the deal's n-th quote, quote_deal_quotes) |
| H12 a quantity of 2^31, a 1e20 price were 500s | bounded inputs: 422 naming the field |
| H13 | open_quote, add_line, send_quote, revise_quote take an Idempotency-Key |
The customer
A quote's customer is a business partner, referenced by bp_id (BIGINT — the shared key in
modules/shared.yaml; account_id means a GL account across modules). When
brick-business-partner is installed — in this process
at tier 1, or at BUSINESS_PARTNER_BASE_URL — a quote is refused for an unknown customer (422) or one that may not take
new business (409, e.g. blocked), checked when the quote is opened AND when it is sent. If that brick is
installed but down, the quote goes ahead and its history says the customer was not checked. Without it, the
bp_id is taken as given.
The deal
A quote on a deal (opportunity_id) is the evidence the pipeline wins it on, so the deal is asked of
brick-pipeline (PIPELINE_BASE_URL or in process) as the caller
when the quote is opened: 404 if they may not see it, 409 if it is another customer's, in another currency, or
closed; 503 while the pipeline is down. Not installed: the id is taken as given, and the history says so. One
quote in flight (draft or sent) per deal.
Who sees what
| Caller | Sees | Changes |
|---|---|---|
rep (quote:quote:draft) | own quotes | own quotes |
quote:quote:read_all (a manager, an analyst) | every quote | own — any, with quote:quote:manage (a manager) |
deal desk (quote:quote:approve) | every quote — they cannot approve what they cannot see | approvals only |
A brick decides by permission, never by role name (erp-platform 0.9.2). From 0.3.0 the table above is permissions; until then the first rows were the manager/analyst role names. The roles are the customer's: | ||
src/brick_quote/roles.yaml ships the defaults, from the CRM spec's actors; a composition maps them to its pool. The | ||
process (application/process.yaml) is served as BPMN 2.0 at GET /process/bpmn. |
A quote the caller may not see is a 404, never a 403. Permissions: quote:quote:draft,
quote:quote:approve, quote:quote:decide, quote:discount_policy:write, quote:price_book:write.
Tables
quote_quotes, quote_versions, quote_lines, quote_line_schedule (the Quote aggregate, saved whole);
quote_events (history, appended); quote_discount_policy, quote_prices (configuration rows);
quote_quotes_versions (0.2.0: the revision claims) and quote_deal_quotes (0.2.0: one quote in flight per deal).
See src/brick_quote/schema.sql. The price book lives here until a second brick needs to write prices.
0.2.0 migration (additive): quote_quotes.revision at the end of the CREATE and as an ALTER (DISRUPTIVE: reads
stall for minutes — migrate before rolling 0.2.0), and the two new tables.
Run
pip install -e '.[test]' # needs erp-platform installed (see its README)
pytest -q # rules + both doors on SQLite, no bucket
BUCKET=<bucket> erp-migrate --schema src/brick_quote/schema.sql --brick quote=0.2.0
BUCKET=<bucket> DEV_ALLOW_ANON=1 DEV_USER_PERMISSIONS=quote:quote:draft \
uvicorn brick_quote.app:app # API at /docs, agents at /mcp
BUCKET=<scratch-bucket> DODIL_ORG=<org> python scripts/live_check.py # the story on DataK3
Deployed, set DODIL_USER_POOL=<org>/<pool> and PUBLIC_BASE_URL (see erp-platform's README).
Not in 0.2
Publishing-specific line fields (issue, placement, event seat, campaigns) — an extension; currency conversion on a line (a line must be in the quote's currency); an approval chain (one level today — spec CRM-06 notes some customers need two); notifying the rep when the desk decides (CRM-11).
License: Apache-2.0, see LICENSE