# CRM — the module source.
#
# ONE file, two renderings: the human page at /modules/crm/ and the agent spec at
# /modules/crm/spec.yaml are both generated from this. Edit here, never there.
#
# This is the REFERENCE vocabulary for Phase 2 (Understand), not a schema to copy. An agent
# starts from these stories and entities and corrects them with the customer; the result is
# written to the customer's own ERP_CRM.md.
id: crm
name: Sales & customer relationships
short: CRM
version: 1
maturity: tested # 14 acceptance/access/invariant tests pass on a bucket — pattern §14
summary: >-
Who your customers are, what you are trying to sell them, and where each deal stands —
shared by the whole sales team instead of living in one person's spreadsheet or inbox.
# ---------------------------------------------------------------------------------------------
# Facets — what an agent filters the catalog on
# ---------------------------------------------------------------------------------------------
facets:
domain: sales
industries: [software, manufacturing, finserv, real-estate] # overlays exist for these
customer_words: # what people actually say when they need this module
- "track our customers"
- "a sales pipeline"
- "stop losing leads"
- "who is talking to which client"
- "quotes and discounts"
- "an app for my sales team"
replaces: [spreadsheets, shared inbox, HubSpot, Salesforce, Pipedrive]
pillars: [sql, graph, vector]
first_module_fit: high # a good first module: visible value in week one, defines the party master
depends_on:
- module: business-partner
why: the customer record is shared with invoicing and purchasing; CRM references it
required: recommended # CRM can run alone on its own accounts; it should not when finance follows
feeds: [ar, gl]
owns_master_data: [] # with business-partner present, CRM owns no shared master data
references_master_data: [business_partner, product, employee]
# ---------------------------------------------------------------------------------------------
# Actors — in business language, mapped to the permission names they need
# ---------------------------------------------------------------------------------------------
actors:
- id: rep
name: Sales rep
does: works their own accounts, leads and deals; drafts quotes
sees: only what they own
permissions: [crm:account:read, crm:account:write, crm:lead:write, crm:opportunity:write, crm:quote:draft, crm:task:write]
- id: manager
name: Sales manager
does: sees the whole team's book, reassigns work, owns the forecast
sees: everything in the team
permissions: [crm:*:read, crm:account:write, crm:opportunity:assign, crm:forecast:override, crm:pipeline_config:write]
- id: deal_desk
name: Deal desk / commercial director
does: approves quotes above the discount threshold, sets the discount policy
sees: every quote
permissions: [crm:quote:approve, crm:discount_policy:write]
- id: marketing
name: Marketing
does: runs campaigns, imports lists, reads attribution
sees: campaigns, leads, attribution — not deal amounts unless granted
permissions: [crm:campaign:write, crm:lead:import, crm:attribution:read]
- id: system
name: The system
does: scheduled and event-driven work — scoring, forecasting, sequences, rollups
sees: everything, through its own service account
permissions: [] # service account, not a pool user
# ---------------------------------------------------------------------------------------------
# Stories — person / system / integration, each with acceptance criteria
# ---------------------------------------------------------------------------------------------
stories:
# person
- id: CRM-01
kind: person
actor: rep
story: As a sales rep, I want to see only the accounts and deals I own, so that my book is not buried under everyone else's.
given: reps Tom and Sofia each own accounts
when: Tom opens Accounts, or opens one of Sofia's accounts by URL
then: he sees only his own; Sofia's returns not found (not forbidden, which would confirm it exists)
screen: Accounts
reference: "partial — crm-suite-app ships the owner_scope helper; wiring it into each owned read is a build step, and many customers choose 'everyone sees everything' instead"
customer_varies: whether rows are owned at all — ask, and record the answer
- id: CRM-02
kind: person
actor: rep
story: As a sales rep, I want to log a call or email against a contact in one step, so that the next person who picks up the account knows what was said.
given: a contact on an account I own
when: I log an activity with a subject and a note
then: it appears on the contact, the account and the open deal, newest first
screen: Account
reference: implemented
- id: CRM-03
kind: person
actor: rep
story: As a sales rep, I want to turn a qualified lead into an account, contact and opportunity at once, so that I do not re-type the same company three times.
given: a lead with a company domain
when: I convert it
then: one account (reused if the domain already exists), one contact, one opportunity in the first stage; converting again changes nothing
screen: Lead
reference: implemented
- id: CRM-04
kind: person
actor: rep
story: As a sales rep, I want to move a deal through our stages, so that the pipeline shows where it really is.
given: an open opportunity in "discovery"
when: I move it to "proposal"
then: its probability and forecast category follow the stage definition, and the change is visible to my manager immediately
screen: Pipeline board
reference: implemented
customer_varies: the stage names, their order and their probabilities are the most commonly changed thing in a CRM
- id: CRM-05
kind: person
actor: rep
story: As a sales rep, I want to build a quote from our price book with line discounts, so that the customer gets a consistent price.
given: an opportunity and an active price book
when: I add three products with a 5% discount on one
then: the quote totals are computed by the system, not typed, and the quote is a draft
screen: Quote builder
reference: implemented
- id: CRM-06
kind: person
actor: deal_desk
story: As the commercial director, I want quotes above our discount threshold routed to me, so that margin is protected without chasing approvals by email.
given: a threshold of 15% and a quote with an 18% effective discount
when: the rep submits it
then: it is needs_approval, it appears in my queue, and the rep cannot approve it; above the maximum discount it is refused outright
screen: Approvals
reference: implemented
customer_varies: the thresholds, and whether approval is one level or a chain (manager, then director)
- id: CRM-07
kind: person
actor: rep
story: As a sales rep, I want an approved quote to be locked, so that what the customer signed is what finance invoices.
given: an approved quote
when: anyone edits a line
then: the edit is refused with a reason, and a new revision must be created instead
screen: Quote builder
reference: implemented
- id: CRM-08
kind: person
actor: manager
story: As a sales manager, I want to see every open deal across a corporate group on one screen, so that a discount given to one subsidiary is not quoted back to another.
given: three subsidiaries of one group owned by different reps
when: I open any one of them
then: I see all three sites' pipeline and past discounts
screen: Account
reference: implemented
- id: CRM-09
kind: person
actor: manager
story: As a sales manager, I want a weighted forecast by rep and by period, so that I can commit a number to leadership.
given: open deals with stages and close dates
when: I open the forecast for this quarter
then: I see open, weighted and committed amounts per rep, and I can override a category with a reason
screen: Forecast
reference: implemented
- id: CRM-12
kind: person
actor: rep
story: As a sales rep, I want my tasks grouped into overdue, today and this week, so that I start the day knowing what is late.
given: tasks assigned to me with due dates
when: I open Home
then: I see counts and lists for overdue, today and this week
screen: Home
reference: implemented
- id: CRM-13
kind: person
actor: marketing
story: As a marketer, I want to import a list of contacts from a spreadsheet, so that a trade-show list becomes leads the same day.
given: a CSV with name, email and company
when: I import it twice
then: each person exists once; rows with no email are reported, not silently dropped
screen: Accounts
reference: implemented
- id: CRM-14
kind: person
actor: rep
story: As a sales rep, I want to find customers similar to one we just won, so that I know who to call next.
given: a won account
when: I ask for lookalikes
then: I get a ranked list of accounts I can see, excluding existing customers
screen: Account
reference: implemented
# system
- id: CRM-10
kind: system
actor: system
trigger: every 6 hours
story: The system re-scores open leads against the current scoring rules, so that the call list reflects this morning's activity rather than last week's.
given: a lead that opened a pricing email an hour ago
when: the next run completes
then: its score reflects it, the run is recorded, and running it twice changes nothing
reference: route exists (POST /leads/{id}/score); the schedule does not — Ignite has no scheduler
- id: CRM-11
kind: system
actor: system
trigger: when a quote is approved or rejected
story: The system notifies the rep who submitted it, so that nobody refreshes a queue waiting.
given: a quote in needs_approval
when: the director approves it
then: the submitting rep is told, once
reference: not implemented — no notification channel in the reference
- id: CRM-15
kind: system
actor: system
trigger: nightly
story: The system snapshots the forecast, so that "what did we think on the 1st" can be answered at quarter end.
given: open pipeline
when: the nightly run completes
then: one snapshot per rep, period and category exists for that date; a re-run replaces it rather than doubling it
reference: route exists (POST /forecast/recompute); the schedule does not
- id: CRM-16
kind: system
actor: system
trigger: nightly
story: The system flags deals that have gone quiet, so that a manager sees risk before the close date passes.
given: a deal with no activity for 21 days in a late stage
when: the nightly run completes
then: it is marked at risk with the reason
reference: route exists (POST /opportunities/{id}/risk); the rule and schedule are yours
- id: CRM-17
kind: system
actor: system
trigger: when a contact is enrolled in a sequence, then on each step's delay
story: The system sends the next step of an outreach sequence when it is due, and stops when the contact replies.
given: a contact enrolled in a three-step sequence
when: step one's delay elapses
then: step one is sent once; a reply stops the enrollment
reference: implemented as a polled engine — needs an always-on worker or an external clock
# integration
- id: CRM-20
kind: integration
actor: system
trigger: when a quote is accepted
story: The system creates a draft invoice in Finance for the same customer and amount, so that nothing is re-keyed.
given: an accepted quote
when: Finance opens drafts
then: the invoice references the quote and the same business partner; posting it is refused if the period is closed
owner: ar # the invoicing module writes invoices; CRM only asks
reference: not implemented — requires the AR module
- id: CRM-21
kind: integration
actor: rep
trigger: when a rep opens an account
story: As a sales rep, I want to see whether the customer has overdue invoices, so that I do not offer a discount to someone who has not paid.
given: a customer with an invoice 40 days past due
when: I open the account
then: I see the overdue amount (read-only), and the quote builder warns me
owner: ar
reference: "not implemented — and the join does not exist yet either: GL's subledger_items carries a free-text party, not bp_id, so either it gains bp_id or invoicing serves this read from its own route (see ar AR-12)"
- id: CRM-22
kind: integration
actor: rep
trigger: when a new company is created
story: The system creates the company once in the shared customer master, so that finance and sales mean the same customer.
given: business-partner is installed
when: a rep converts a lead for a new domain
then: a business partner exists (created or reused), and the CRM account points at it
owner: business-partner
reference: implemented (POST /partners/promote)
# ---------------------------------------------------------------------------------------------
# Workflows — per entity with a lifecycle
# ---------------------------------------------------------------------------------------------
workflows:
- entity: lead
states: [new, working, qualified, disqualified, converted]
transitions:
- { from: new, to: working, by: rep }
- { from: working, to: qualified, by: [rep, system], gate: "score >= policy.threshold_qualify" }
- { from: [new, working], to: disqualified, by: rep, requires: reason }
- { from: qualified, to: converted, by: rep, effect: "creates account + contact + opportunity (CRM-03)" }
- entity: opportunity
states: "defined per customer in pipeline_stages (name, order, probability, forecast category, won/closed flags)"
transitions:
- { from: any open, to: any open, by: [rep, manager] }
- { from: any open, to: closed_won, by: rep, effect: "customer_varies: often creates the order/invoice (CRM-20)" }
- { from: any open, to: closed_lost, by: rep, requires: loss_reason }
- entity: quote
states: [draft, needs_approval, approved, rejected, accepted, expired]
transitions:
- { from: draft, to: approved, by: system, gate: "effective discount <= approval_threshold_pct" }
- { from: draft, to: needs_approval, by: system, gate: "approval_threshold_pct < discount <= max_discount_pct" }
- { from: draft, to: rejected, by: system, gate: "discount > max_discount_pct" }
- { from: needs_approval, to: [approved, rejected], by: deal_desk }
- { from: approved, to: accepted, by: rep, effect: "locked (CRM-07); triggers CRM-20" }
- { from: [draft, approved], to: expired, by: system, gate: "valid_until passed" }
- entity: task
states: [open, done, cancelled]
transitions:
- { from: open, to: [done, cancelled], by: [assignee, manager] }
- { from: [done, cancelled], to: open, by: [assignee, manager] }
# ---------------------------------------------------------------------------------------------
# Entities — business meaning first, then the reference table
# ---------------------------------------------------------------------------------------------
entities:
- id: account
meaning: a company you sell to (one row per site/legal entity)
natural_key: org_domain in the reference; bp_id when business-partner is installed
key_attributes: [name, parent, tier, country, industry, owner]
relationships: ["subsidiary_of -> account (typed edge)", "1..n contact", "1..n opportunity"]
master_data: references business_partner
reference_table: crm_accounts
- id: contact
meaning: a person at an account
natural_key: email
key_attributes: [full_name, title, lifecycle_stage, owner, subscribed]
relationships: ["-> account", "1..n activity"]
master_data: module-owned
reference_table: contacts
recommended_table: crm_contacts
- id: lead
meaning: a person or company not yet worked into a deal
natural_key: lead_id (derive it from email or domain so a re-import is idempotent)
key_attributes: [source, status, owner, score]
master_data: module-owned
reference_table: leads
recommended_table: crm_leads
- id: opportunity
meaning: a specific deal you are trying to win
natural_key: opportunity_id
key_attributes: [account, amount, stage, close_date, owner, forecast_category, probability]
relationships: ["-> account", "-> primary contact", "1..n quote", "0..1 lead_score"]
master_data: module-owned
reference_table: opportunities
recommended_table: crm_opportunities
- id: activity
meaning: a call, email, meeting or note
natural_key: activity_id
key_attributes: [kind, subject, direction, ts, owner]
relationships: ["-> contact", "-> opportunity"]
master_data: module-owned
reference_table: activities
recommended_table: crm_activities
- id: pipeline_stage
meaning: one step of YOUR sales process, as data
natural_key: "pipeline:name"
key_attributes: [position, probability, forecast_category, is_won, is_closed]
master_data: module-owned (configuration)
reference_table: pipeline_stages
- id: product
meaning: something you sell
natural_key: product_id (or SKU)
key_attributes: [name, sku, category, unit, list_price, active]
master_data: SHARED — sales quotes it, invoicing bills it, purchasing buys it
reference_table: products
note: the reference lets CRM write it; once a second module needs products, give it one owner
- id: price_book
meaning: a price list for a currency/region, with per-product prices
natural_key: price_book_id; entry_id
master_data: module-owned
reference_table: [price_books, price_book_entries]
- id: quote
meaning: a priced offer with lines
natural_key: quote_id; quote_line_id
key_attributes: [status, currency, subtotal, discount_total, total, valid_until]
relationships: ["-> opportunity", "-> account", "-> price_book", "1..n quote_line -> product"]
master_data: module-owned
reference_table: [quotes, quote_lines]
- id: discount_policy
meaning: the thresholds that decide what auto-approves
natural_key: policy_id
master_data: module-owned (configuration; changing it is a management act)
reference_table: discount_policy
- id: lead_score
meaning: how good a lead is, and why (BANT or MEDDIC)
natural_key: lead_id
master_data: module-owned (derived)
reference_table: [lead_scores, scoring_policy]
- id: campaign
meaning: a marketing push, its members and touchpoints
natural_key: campaign_id; member_id; touchpoint_id
master_data: module-owned
reference_table: [campaigns, campaign_members, touchpoints, attribution]
- id: task
meaning: a to-do for a person, attached to any record
natural_key: task_id
master_data: module-owned (a candidate for a shared "work" module later)
reference_table: tasks
- id: forecast_snapshot
meaning: the forecast as it stood on a date
natural_key: snapshot_id
master_data: module-owned (derived)
reference_table: [forecast_snapshots, deal_risk]
- id: sequence
meaning: an automated outreach cadence
natural_key: flow_id; step_id; enrollment_id
master_data: module-owned
reference_table: [flows, flow_steps, flow_enrollments, flow_actions]
optional: true
# ---------------------------------------------------------------------------------------------
# Master data — what this module owns vs only references
# ---------------------------------------------------------------------------------------------
master_data:
owns: []
references:
- entity: business_partner
owner: business-partner
how: crm_accounts.account_id = business_partner.bp_id
if_absent: CRM keys accounts on web domain alone — fine for a sales-only system, a problem the day finance arrives
- entity: product
owner: "decide per customer — the reference lets CRM write it"
how: quote_lines.product_id
- entity: employee
owner: "the pool (identity) — rep/owner columns hold the pool user's sub or email"
how: owner columns
warning: >-
The most expensive mistake is a CRM-only customer list. If invoicing or purchasing is on the
roadmap at all, install business-partner first and let CRM reference it.
# ---------------------------------------------------------------------------------------------
# Jobs — every system story, and where it can run (Ignite has no scheduler)
# ---------------------------------------------------------------------------------------------
jobs:
- story: CRM-10
trigger: every 6 hours
runs: re-score open leads
where_options: [always-on worker app (--reserved 1 --max-replicas 1) with its own loop, external scheduler calling a route]
idempotent: required
- story: CRM-15
trigger: nightly
runs: POST /forecast/recompute
where_options: [always-on worker app, external scheduler]
idempotent: required
- story: CRM-16
trigger: nightly
runs: risk rule over late-stage opportunities
where_options: [always-on worker app, external scheduler]
idempotent: required
- story: CRM-17
trigger: continuous (per-step delays)
runs: sequence engine
where_options: [always-on worker app — polling is the only option without a scheduler]
idempotent: required
- story: CRM-11
trigger: on quote decision
runs: notify submitter
where_options: [inline in the approval route]
# ---------------------------------------------------------------------------------------------
# Seams — work that crosses into another module, and who owns it
# ---------------------------------------------------------------------------------------------
seams:
- story: CRM-20
with: ar
owner: ar
tested: "code/crm-suite-app/v1/tests/test_suite.py — accepting a quote raises the invoice, once, at the right money"
rule: CRM calls the invoicing module's route; it never writes invoice tables
- story: CRM-21
with: ar
owner: ar
rule: read-only join through business_partner
- story: CRM-22
with: business-partner
owner: business-partner
rule: CRM calls promote; it never writes business_partner
# ---------------------------------------------------------------------------------------------
# Screens — derived from the person stories, not invented in the build (pattern §8)
# ---------------------------------------------------------------------------------------------
screens:
- { id: home, name: Home, stories: [CRM-12, CRM-16], actors: [rep, manager] }
- { id: accounts, name: Accounts, stories: [CRM-01, CRM-13], actors: [rep, manager] }
- { id: account_360, name: Account, stories: [CRM-02, CRM-08, CRM-14, CRM-21], actors: [rep, manager] }
- { id: pipeline, name: Pipeline board, stories: [CRM-04], actors: [rep, manager] }
- { id: lead, name: Lead, stories: [CRM-03, CRM-10], actors: [rep, marketing] }
- { id: quote, name: Quote builder, stories: [CRM-05, CRM-07, CRM-20], actors: [rep] }
- { id: approvals, name: Approvals, stories: [CRM-06, CRM-11], actors: [deal_desk] }
- { id: forecast, name: Forecast, stories: [CRM-09, CRM-15], actors: [manager] }
- { id: campaigns, name: Campaigns, stories: [CRM-17], actors: [marketing], optional: true }
- { id: settings, name: Settings, stories: [], actors: [manager, deal_desk],
note: "every config_row below lives here — this screen is what is where a customer configures the module before extending or owning any code" }
# ---------------------------------------------------------------------------------------------
# Permissions — the pool role catalog this module MERGES in (pattern §4)
# ---------------------------------------------------------------------------------------------
permissions:
catalog:
- crm:account:read
- crm:account:write
- crm:lead:write
- crm:opportunity:write
- crm:opportunity:assign
- crm:quote:draft
- crm:quote:approve
- crm:discount_policy:write
- crm:pipeline_config:write
- crm:forecast:override
- crm:campaign:write
- crm:attribution:read
- crm:task:write
# WHAT THE CODE ENFORCES TODAY. The catalog above is the target shape; these names predate
# the <module>:<object>:<verb> rule and are what crm-suite-app actually checks. Renaming them
# is a breaking change for any pool whose role catalog already carries them, so both are
# published until that migration happens.
enforced_today:
- orgs:qualify
- leads:score
- quotes:approve
- quotes:accept
- forecast:override
row_visibility:
rule: "a rep sees rows they own; a manager sees the team's"
enforced: "every owned read is scoped by the caller's identity in the route"
not_found_not_forbidden: true
# ---------------------------------------------------------------------------------------------
# Configuration rows — what the customer changes WITHOUT a code change (pattern §6).
# The test for each: could their office manager change it on the Settings screen?
# ---------------------------------------------------------------------------------------------
config_rows:
- { table: pipeline_stages, changes: "the stages of your sales process, their order, probability and forecast category" }
- { table: discount_policy, changes: "the discount that auto-approves, the maximum, and who approves between them" }
- { table: price_books, changes: "price lists per currency or region" }
- { table: scoring_policy, changes: "how a lead is qualified (BANT, MEDDIC or your own weights and thresholds)" }
- { table: crm_settings, changes: "quote validity, follow-up days, loss reasons, company details on the PDF" }
in_code_not_rows:
- "which transitions exist and who may make them"
- "that a sent quote freezes and edits create a revision"
- "that the discount cap is checked on every edit AND again when the quote is sent"
- "that a lost deal requires a reason"
# ---------------------------------------------------------------------------------------------
# Tests the build must produce — one per story, plus access and invariants (pattern §9)
# ---------------------------------------------------------------------------------------------
tests:
acceptance: "one per story id above, named for it"
access:
- "a rep opening another rep's account gets not found, not forbidden"
- "a rep cannot approve a quote above the threshold"
- "a request with no gateway identity is rejected"
- "a forged X-Dodil-User header is stripped at the edge"
invariants:
- "quote totals are computed, never typed — line discounts and header discount compose"
- "an approved quote refuses edits; a revision is a new row"
- "re-importing the same contacts changes nothing"
- "amounts are never summed across currencies"
- "every job is idempotent: a second run changes nothing"
# ---------------------------------------------------------------------------------------------
# Scale — what this module hits first on DataK3 (pattern §10)
# ---------------------------------------------------------------------------------------------
scale:
reads: "the tables reader serves 16 concurrent reads; hold a client-side semaphore below it (DB_CONCURRENCY=10) and retry on SQLSTATE 40001/08006/58030/XX000 plus the message markers for misclassified blips"
hot_paths: ["pipeline board (all open opportunities + stage config)", "account 360 (family rollup via the graph)", "forecast recompute"]
workers: "any polling engine (sequences, re-scoring) runs pinned warm: --reserved 1 --max-replicas 1"
volumes: "designed for tens of thousands of accounts and hundreds of thousands of activities; above that, snapshot the forecast rather than recomputing on read"
# ---------------------------------------------------------------------------------------------
# What customers usually change — the questions to ask in Phase 2
# ---------------------------------------------------------------------------------------------
# What differs between customers — with something for them to react to, and where one exists,
# something they can look up.
#
# A business user can rarely recall a policy on demand and can always react to one. "Most firms
# like yours chase at 7, 14 and 30 days — does that match?" gets a truer answer, faster, than
# "when do you chase?". Read it out, let them correct it, write down what they said.
#
# Two kinds of answer, and they must never be confused:
#
# standard: a NAMED external authority the customer can look up and hold us to — a
# directive, an accounting standard, a statute. Adopt it, or decide deliberately
# to differ and know why. If a line here cannot be cited, it does not belong.
# typical: arrangements we have SEEN. No authority, no survey behind them, no implication
# that a customer doing otherwise is doing it wrong. Conversation, not evidence.
#
# Calling convention a standard is how an implementation ends up defending a choice nobody
# ever made.
customer_varies:
- question: What are the steps of your sales process, and how likely is a deal to close at each?
changes: pipeline_stages
typical:
b2b_considered_sale: qualify, discovery, proposal, negotiation, closed — five or six stages
transactional: "two or three: enquiry, quote, won"
warning: a stage nobody can define the EXIT criteria for is a stage that will not be updated
- question: Who can give what discount, and who approves above that?
changes: discount_policy, the quote workflow (single or chained approval)
typical:
common: a rep discounts to ~10%, a manager to ~25%, anything beyond needs the owner
note: the numbers matter less than there being exactly one threshold per approver
- question: Does each rep only see their own customers, or does everyone see everything?
changes: the actors' "sees", CRM-01
- question: Do you sell to groups of companies (parents and subsidiaries)?
changes: the account family edge, CRM-08
- question: Do you send quotes and invoices from this system, or elsewhere?
changes: whether CRM-20/21 are in scope; whether business-partner is needed now
- question: What do you use today, and what must come across on day one?
changes: CRM-13 import mapping, migration scope
- question: Do you run marketing campaigns or outreach sequences?
changes: whether campaign and sequence entities are in scope at all (both optional)
- question: How do you qualify a lead?
changes: scoring_policy (BANT, MEDDIC or your own), CRM-10
typical:
smaller_teams: budget, authority, need, timing (BANT) — four fields on the lead
enterprise: MEDDIC, because the economic buyer and the paper process are the risk
self_serve: usage or firmographic score, not a questionnaire
# ---------------------------------------------------------------------------------------------
# The reference implementation — phase 4 only
# ---------------------------------------------------------------------------------------------
reference:
# Public repos, anonymous clone, one release tag each. The old code/ packages are retired (archive/code).
repos:
- name: brick-activity
role: brick
https: https://git.dodil.io/dodil/brick-activity.git
ssh: git@git.dodil.io:dodil/brick-activity.git
tag: v0.2.0
read_first: [README.md, AGENTS.md]
- name: brick-pipeline
role: brick
https: https://git.dodil.io/dodil/brick-pipeline.git
ssh: git@git.dodil.io:dodil/brick-pipeline.git
tag: v0.3.0
read_first: [README.md, AGENTS.md]
- name: brick-quote
role: brick
https: https://git.dodil.io/dodil/brick-quote.git
ssh: git@git.dodil.io:dodil/brick-quote.git
tag: v0.2.0
read_first: [README.md, AGENTS.md]
- name: brick-lead
role: brick
https: https://git.dodil.io/dodil/brick-lead.git
ssh: git@git.dodil.io:dodil/brick-lead.git
tag: v0.2.0
read_first: [README.md, AGENTS.md]
- name: brick-business-partner
role: brick
https: https://git.dodil.io/dodil/brick-business-partner.git
ssh: git@git.dodil.io:dodil/brick-business-partner.git
tag: v0.3.0
read_first: [README.md, AGENTS.md]
- name: erp-platform
role: platform
https: https://git.dodil.io/dodil/erp-platform.git
ssh: git@git.dodil.io:dodil/erp-platform.git
tag: v0.5.3
read_first: [README.md, AGENTS.md]
- name: erp-ui
role: ui
https: https://git.dodil.io/dodil/erp-ui.git
ssh: git@git.dodil.io:dodil/erp-ui.git
tag: v0.2.0
read_first: [README.md, AGENTS.md]
packages: []
contracts: [] # the skill-contract tier is retired; the spec IS the contract
build_guide: /library/crm-reference-build
known_gaps:
- "none of the four CRM bricks ships a screen: API and MCP only. erp-ui supplies the components the screens are built from"
- "activity: no mailbox or calendar sync, no @mentions, no private notes, no merging of duplicate contacts"
- "pipeline: one pipeline only; no probability that decays with a deal's age; the forecast does not convert currencies"
- "quote: no publishing-specific line fields; a line must be in the quote's currency; one approval level (CRM-06 notes some customers need two); the rep is not notified when the deal desk decides (CRM-11)"
- "lead: lookalikes rank on a fit-profile vector, not on language-model embeddings; no score decay inside the window; no reopen from disqualified; no merge of a person lead into a company lead; a customer whose bp_key is not its domain is not found by domain"
- "across apps (tier 2) the acting person is not carried to the next brick: a converted lead's deal is owned by the lead brick's service identity, and nothing is auto-attached to a deal by the activity brick (the on-behalf-of gap in erp_platform.seam)"
- "the nightly jobs (forecast snapshot, lead scoring) need something to run them: the platform has no scheduler"
- "the join the party master exists to make possible needs a CAST while invoicing's bp_id column holds a bp_KEY. The un-cast join FAILS outright rather than returning wrong rows - design/ar-bp-id-change-request.md"