dodil/erp-brick-pipeline

README.md

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

StoryHow
CRM-01 a rep sees only their dealsowner-scoped; another rep's deal is 404. Managers and analysts see all
CRM-04 move a deal through our stagesstages are rows (name, order, probability, forecast category, won/closed). A move takes the stage's probability unless the deal carries its own
won / lostwon 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 pipelineasks brick-business-partner for the corporate family and shows every open deal in it
CRM-09 weighted forecast by rep and quarteropen, 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 dealopen_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 quietderived 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)

FindingNow
C1 a quote for another customer won this dealonly 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_refsource_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 edita 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
C4bp_id (and group) leave as strings of digits; come in as digits or a number
H1 won kept the typed estimatewon 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 overclosed 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 500sbounded inputs: 422 naming the field
H13open_opportunity takes an Idempotency-Key

Neighbours

BrickAsked forIf not installedIf 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 checkedwon 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 familybp_id taken as given; the group view shows this partner onlya 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