brick-pipeline
The sales pipeline: deals, your stages as data, the weighted forecast and its snapshots. A brick on
erp-platform, rebuilt from a general CRM opportunity model and
held to the CRM spec (modules/crm). Every action is both an API route and an MCP tool.
What it does
| Story | How |
|---|---|
| CRM-01 a rep sees only their deals | owner-scoped; another rep's deal is 404. Managers and analysts see all |
| CRM-04 move a deal through our stages | stages are rows (name, order, probability, forecast category, won/closed). A move takes the stage's probability unless the deal carries its own |
| won / lost | won needs an accepted quote from this deal's customer, in its currency (asked of brick-quote), and the deal takes that quote's total (0.3.0). Lost needs a reason from the agreed list, so losses can be counted. A closed deal is final: a manager reopen_opportunitys it with a reason, which voids the quotes that won it |
| CRM-08 the whole group's pipeline | asks brick-business-partner for the corporate family and shows every open deal in it |
| CRM-09 weighted forecast by rep and quarter | open, weighted, committed and won, per currency, computed on read; a manager overrides a category with a reason |
| CRM-15 "what did we think on the 1st?" | take_forecast_snapshot for a date; a re-run replaces that date, deleting rows that no longer apply |
| CRM-03 a lead converted twice is one deal | open_opportunity_from_source (POST /opportunities/from-source, 0.3.0) takes a source_ref ("lead:<lead_id>") and the owner: the same ref always returns the same deal, so a conversion retried after its first reply was lost does not open a second — and a ref that already opened a deal for ANOTHER partner is 409, nothing written. The ref is claimed with a plain INSERT into pipeline_source_refs (a duplicate fails 23505 → the winner is read), never an upsert. It is a route for another brick only (tool=False), behind pipeline:opportunity:link_source, which no person role holds |
| CRM-16 deals gone quiet | derived on read: at or past the risk probability with nothing logged for N days, or past the close date. The activity brick reports activity through touch_opportunity |
A won deal's amount, currency and customer are the contract's and cannot be edited; a closed deal's amount and close date are frozen. A stage that still holds deals cannot be removed from the pipeline.
0.3.0 — the CRM audit's wave 1 (2026-10-17)
| Finding | Now |
|---|---|
| C1 a quote for another customer won this deal | only an accepted quote with this deal's bp_id and currency, and not voided by a reopen, is evidence. brick-quote 0.2 says which customer accepted each quote (quote-facts.accepted); an older quote brick is 503 for a win |
C2 a person planted a deal under someone else's lead's source_ref | source_ref is gone from open_opportunity (route body and MCP schema); open_opportunity_from_source is a seam-only route behind pipeline:opportunity:link_source (the lead brick's seam at tier 1, its service client at tier 2). A reused ref for another partner is 409 |
| C3 two people editing one deal: 6 of 6 lost an edit | a deal carries a version (GET sends it as the ETag); edit, move, override, reopen take version or If-Match and answer 409 "changed since you opened it" when stale; without one, a lost race is re-run on the latest |
| C4 | bp_id (and group) leave as strings of digits; come in as digits or a number |
| H1 won kept the typed estimate | won takes the accepted quotes' total (same currency) and records which quotes (won_quote_ids) |
| H3 a won deal could be moved back and forth, its close date typed over | closed won / lost are final; reopen_opportunity (pipeline:opportunity:reopen, a manager's) with a reason; it voids a won deal's quotes; close date and amount frozen on a closed deal |
H12 1e20 amounts, snapshots/not-a-date, 23-digit ids were 500s | bounded inputs: 422 naming the field |
| H13 | open_opportunity takes an Idempotency-Key |
Neighbours
| Brick | Asked for | If not installed | If down |
|---|---|---|---|
quote (QUOTE_BASE_URL or in-process) | GET /opportunities/{id}/quote-facts — accepted? for how much? | won is recorded and the history says it was not checked | won is refused (503) until it answers: a won deal is the number leadership is given |
business-partner (BUSINESS_PARTNER_BASE_URL or in-process) | the customer, and its family | bp_id taken as given; the group view shows this partner only | a deal still opens; the group view says it was not resolved |
The pipeline's service identity needs quote:opportunity:read on the quote brick.
Who
Permissions: pipeline:opportunity:write (reps), pipeline:forecast:override (managers),
pipeline:opportunity:reopen (managers, 0.3.0), pipeline:forecast:snapshot (whatever runs the nightly job —
there is no platform scheduler), pipeline:stages:write (sales ops), pipeline:opportunity:touch (the activity
brick's service identity), pipeline:opportunity:link_source (the lead brick's service identity ONLY — no person
role, 0.3.0). From 0.3.1 it is a machine permission (Brick(machine_permissions=…), erp-platform 0.9.2): it passes
on an exact grant only, so a wildcard role (*:*:*, pipeline:*:*) runs every person command and never this one.
The process (application/process.yaml, 0.3.1) is served as BPMN 2.0 at GET /process/bpmn.
A brick decides by permission, never by role name (erp-platform 0.9.2). From 0.4.0: seeing every deal (and the whole forecast) is pipeline:opportunity:read_all, changing anyone's pipeline:opportunity:manage — until then the manager/analyst role names. The roles are the customer's:
src/brick_pipeline/roles.yaml ships the defaults, from the CRM spec's actors; a composition maps them to its pool.
Tables
pipeline_opportunities, pipeline_events (history), pipeline_settings + pipeline_stages (the sales
process), pipeline_snapshot_runs + pipeline_snapshot_rows (CRM-15), pipeline_source_refs (0.2.0: which deal a
source document opened — an additive table; migrate it before deploying 0.2.0), pipeline_opportunities_versions
(0.3.0, the optimistic-version claims; with three columns at the end of pipeline_opportunities — won_quote_ids,
voided_quote_ids, version — added by ALTERs that erp-migrate marks DISRUPTIVE: migrate before rolling 0.3.0).
See src/brick_pipeline/schema.sql.
Run
pip install -e '.[test]' # needs erp-platform; brick-quote and brick-business-partner
pytest -q # installed too run the tier-1 tests across all three
BUCKET=<scratch> DODIL_ORG=<org> python scripts/live_check.py
The lead brick (brick-lead) converts a lead into partner + contact + deal (CRM-03) by calling
POST /opportunities/from-source with source_ref and owner; its service identity needs
pipeline:opportunity:link_source.
Not in 0.3
Multiple pipelines; a probability that decays with age; currency conversion in the forecast.
License: Apache-2.0, see LICENSE