dodil/erp-method

node_modules/.cache/erp-method/v0.1.20/modules/crm/module.yaml

# 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.

start_here: >-
  START HERE, before reading on: dodil://erp/build is the map (read it first; this spec is a vocabulary,
  not the method). Then, in the project folder, two commands: (1) `uvx --from git+https://git.dodil.io/dodil/erp-platform.git@v0.8.2 erp-compose init crm`;
  (2) SHOW the customer the diagram: `uvx --from git+https://git.dodil.io/dodil/erp-platform.git@v0.8.2 erp-compose bpmn --from ERP_CRM.md
  --out processes/crm.bpmn` (before ERP_CRM.md exists: `--from` this spec's
  module.yaml), then paste its step names in order, with who does each, into your message. The signed
  artifact is that BPMN: never prose, never "open bpmn.io". Cards: dodil://erp/phase/show, dodil://erp/phase/signoff.
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/erp-brick-activity.git
      ssh: git@git.dodil.io:dodil/erp-brick-activity.git
      tag: v0.2.0
      read_first: [README.md, AGENTS.md]
    - name: brick-pipeline
      role: brick
      https: https://git.dodil.io/dodil/erp-brick-pipeline.git
      ssh: git@git.dodil.io:dodil/erp-brick-pipeline.git
      tag: v0.3.0
      read_first: [README.md, AGENTS.md]
    - name: brick-quote
      role: brick
      https: https://git.dodil.io/dodil/erp-brick-quote.git
      ssh: git@git.dodil.io:dodil/erp-brick-quote.git
      tag: v0.2.0
      read_first: [README.md, AGENTS.md]
    - name: brick-lead
      role: brick
      https: https://git.dodil.io/dodil/erp-brick-lead.git
      ssh: git@git.dodil.io:dodil/erp-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/erp-brick-business-partner.git
      ssh: git@git.dodil.io:dodil/erp-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"