Modules / CRM
Sales & customer relationships
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.
People ask for it as: “track our customers”, “a sales pipeline”, “stop losing leads”, “who is talking to which client”, “quotes and discounts”, “an app for my sales team”
Who uses it
| Role | Does | Sees |
|---|---|---|
| Sales rep | works their own accounts, leads and deals; drafts quotes | only what they own |
| Sales manager | sees the whole team's book, reassigns work, owns the forecast | everything in the team |
| Deal desk / commercial director | approves quotes above the discount threshold, sets the discount policy | every quote |
| Marketing | runs campaigns, imports lists, reads attribution | campaigns, leads, attribution — not deal amounts unless granted |
| The system | scheduled and event-driven work — scoring, forecasting, sequences, rollups | everything, through its own service account |
What it has to do
Reference user stories. Each has a test written into it — the given / when / then — which becomes an automated check once it is built. Expect to keep some, change some and drop some.
Things people do
CRM-01As 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)
Sales rep · 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
Usually different per customer: whether rows are owned at all — ask, and record the answer
CRM-02As 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
Sales rep · screen: Account · reference: implemented
CRM-03As 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
Sales rep · screen: Lead · reference: implemented
CRM-04As 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
Sales rep · screen: Pipeline board · reference: implemented
Usually different per customer: the stage names, their order and their probabilities are the most commonly changed thing in a CRM
CRM-05As 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
Sales rep · screen: Quote builder · reference: implemented
CRM-06As 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
Deal desk / commercial director · screen: Approvals · reference: implemented
Usually different per customer: the thresholds, and whether approval is one level or a chain (manager, then director)
CRM-07As 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
Sales rep · screen: Quote builder · reference: implemented
CRM-08As 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
Sales manager · screen: Account · reference: implemented
CRM-09As 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
Sales manager · screen: Forecast · reference: implemented
CRM-12As 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
Sales rep · screen: Home · reference: implemented
CRM-13As 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
Marketing · screen: Accounts · reference: implemented
CRM-14As 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
Sales rep · screen: Account · reference: implemented
Things that happen on their own
CRM-10every 6 hoursThe 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
CRM-11when a quote is approved or rejectedThe 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
CRM-15nightlyThe 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
CRM-16nightlyThe 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
CRM-17when a contact is enrolled in a sequence, then on each step's delayThe 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
Things that cross into other modules
CRM-20when a quote is acceptedThe 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
· owned by ar · reference: not implemented — requires the AR module
CRM-21when a rep opens an accountAs 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
· owned by 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)
CRM-22when a new company is createdThe 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
· owned by business-partner · reference: implemented (POST /partners/promote)
How things move
lead
new → working → qualified → disqualified → converted
- new → working by rep
- working → qualified by rep or system · when score >= policy.threshold_qualify
- new / working → disqualified by rep · needs reason
- qualified → converted by rep · creates account + contact + opportunity (CRM-03)
opportunity
defined per customer in pipeline_stages (name, order, probability, forecast category, won/closed flags)
- any open → any open by rep or manager
- any open → closed_won by rep · customer_varies: often creates the order/invoice (CRM-20)
- any open → closed_lost by rep · needs loss_reason
quote
draft → needs_approval → approved → rejected → accepted → expired
- draft → approved by system · when effective discount <= approval_threshold_pct
- draft → needs_approval by system · when approval_threshold_pct < discount <= max_discount_pct
- draft → rejected by system · when discount > max_discount_pct
- needs_approval → approved / rejected by deal_desk
- approved → accepted by rep · locked (CRM-07); triggers CRM-20
- draft / approved → expired by system · when valid_until passed
task
open → done → cancelled
- open → done / cancelled by assignee or manager
- done / cancelled → open by assignee or manager
What it keeps
| Thing | Meaning | Identified by | Shared? |
|---|---|---|---|
| account | a company you sell to (one row per site/legal entity) | org_domain in the reference; bp_id when business-partner is installed | references business_partner |
| contact | a person at an account | email | module-owned |
| lead | a person or company not yet worked into a deal | lead_id (derive it from email or domain so a re-import is idempotent) | module-owned |
| opportunity | a specific deal you are trying to win | opportunity_id | module-owned |
| activity | a call, email, meeting or note | activity_id | module-owned |
| pipeline stage | one step of YOUR sales process, as data | pipeline:name | module-owned (configuration) |
| product | something you sell | product_id (or SKU) | SHARED — sales quotes it, invoicing bills it, purchasing buys it |
| price book | a price list for a currency/region, with per-product prices | price_book_id; entry_id | module-owned |
| quote | a priced offer with lines | quote_id; quote_line_id | module-owned |
| discount policy | the thresholds that decide what auto-approves | policy_id | module-owned (configuration; changing it is a management act) |
| lead score | how good a lead is, and why (BANT or MEDDIC) | lead_id | module-owned (derived) |
| campaign | a marketing push, its members and touchpoints | campaign_id; member_id; touchpoint_id | module-owned |
| task | a to-do for a person, attached to any record | task_id | module-owned (a candidate for a shared "work" module later) |
| forecast snapshot | the forecast as it stood on a date | snapshot_id | module-owned (derived) |
| sequence (optional) | an automated outreach cadence | flow_id; step_id; enrollment_id | module-owned |
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.
What runs on its own
| Story | When | What |
|---|---|---|
| CRM-10 | every 6 hours | re-score open leads |
| CRM-15 | nightly | POST /forecast/recompute |
| CRM-16 | nightly | risk rule over late-stage opportunities |
| CRM-17 | continuous (per-step delays) | sequence engine |
| CRM-11 | on quote decision | notify submitter |
Questions that shape your version
Read these out and let the customer correct one — people rarely recall a policy on demand and can always react to one. Standard is a named authority you can look up and hold us to. Commonly seen is exactly that: arrangements we have met, with no survey behind them. A customer doing none of them is not doing it wrong.
- What are the steps of your sales process, and how likely is a deal to close at each?
- 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
- Who can give what discount, and who approves above that?
- 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
- Does each rep only see their own customers, or does everyone see everything?
- Do you sell to groups of companies (parents and subsidiaries)?
- Do you send quotes and invoices from this system, or elsewhere?
- What do you use today, and what must come across on day one?
- Do you run marketing campaigns or outreach sequences?
- How do you qualify a lead?
- 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
What you can change yourself
These are settings, not code: someone in your team changes them on a screen, without a developer and without a release.
- the stages of your sales process, their order, probability and forecast category
- the discount that auto-approves, the maximum, and who approves between them
- price lists per currency or region
- how a lead is qualified (BANT, MEDDIC or your own weights and thresholds)
- quote validity, follow-up days, loss reasons, company details on the PDF
For builders
- Module spec — this page, for agents
- The lifecycle and the design template
- crm-suite-app — all of the above in one FastAPI app + React UI
- crm-core — accounts, contacts, deals, activities only
- How the reference is built
Known gaps in the reference
- the test suite covers 5 of 20 stories: activities, stages, quoting, the discount cap and tasks, plus access and invariants. The rest need work in the package first — the list is in tests/README.md
- CRM-20/21/22 are BUILT and tested against the other three modules in one bucket (tests/test_suite.py, 10 tests) — accepting a quote promotes the party, raises a draft invoice and reaches the ledger
- the join the party master exists to make possible needs a CAST, because invoicing's bp_id column actually holds a bp_KEY. The un-cast join FAILS outright (Could not convert string … to INT64) rather than returning wrong rows — design/ar-bp-id-change-request.md
- POST /quote-cpq/quote_lines accepts a line for a quote that does not exist and answers 200, leaving a line with no parent
- the hand-off to finance mints its own app claims; an app-to-app call belongs to a dodil-appid service client, so invoicing cannot yet tell a service from a person
- `discount_pct` holds a FRACTION (0.18 = 18%) despite its name: sending 18 silently produces a negative line total and a rejected quote
- tables are mostly unprefixed (contacts, quotes, products) — prefix the module-owned ones for a shared bucket
- no scheduler for CRM-10/15/16/17
- no notifications (CRM-11)
- permission names predate the <module>:<object>:<verb> rule (quotes:approve)