Chapter 1 · Accounts, contacts and leads
One customer, one record
A customer relationship tool keeps who your customers are and where each sale stands. This chapter shows what it replaces, why a company is stored once for the whole business, and how a lead becomes an account, a contact and a deal without losing where it came from.
25 minAccounts on core_party4 record shapesLead row kept
By the end of this chapter you can
- Say what the CRM module covers, what it leaves to other modules and which subscriptions it replaces.
- Explain why an account points at the shared party table instead of holding its own company record.
- Describe what lead conversion creates and what it must never change.
- Name the consent and do-not-contact fields and the written rule that merges duplicates.
What the module replaces
The module manages the revenue pipeline from a first enquiry to a won order: accounts and contacts, leads and how they are qualified, deals that move through stages, the calls, emails and meetings around them, and quotes priced from a price book or estimated from a routing and a bill of materials. It replaces the customer relationship management tools a business usually pays for: Salesforce Sales Cloud, HubSpot Sales Hub, Microsoft Dynamics 365 Sales, Pipedrive, Zoho CRM and Close, and the quoting tools Salesforce Revenue Cloud, DealHub and Paperless Parts. HubSpot describes its data as a small set of linked objects such as contacts, companies and deals [1]. This module keeps the same objects, but on the platform's own spine, so a customer is one record that sales, service and billing share.
| Inside this module | Left to another module | Owner |
|---|---|---|
| Accounts, contacts, leads, conversion and duplicate merging | Marketing campaigns, scoring and nurture | M16 |
| Pipelines, stages, stage history, contact roles, forecasting | Support tickets | M19 |
| Activities, price books, quotes, approvals, acceptance | Order fulfilment and invoicing | M12 and accounting |
| Routing and bill-of-materials cost estimates for made-to-order quotes | Building the routing and the bill of materials | M04 and M14 |
The specification lists fifteen crm_ tables. This course builds thirteen of them: crm_account, crm_contact, crm_lead, crm_pipeline, crm_pipeline_stage, crm_opportunity, crm_stage_history, crm_contact_role, crm_activity, crm_price_book, crm_price_book_entry, crm_quote and crm_quote_line. Chapter 2 lists every column of the thirteen. The other two, crm_cost_estimate and crm_territory, are explained in chapters 2 and 3 so you can add them later.
A company is stored once
Every business has one list of companies and people. The Foundation spine already holds it as core_party (a company or organisation) and core_person (a human). A CRM account is not a second company record. It is a crm_account row whose party_id points at the one core_party row, one to one, and adds only what sales needs: the account_type (prospect, customer, partner, competitor or distributor), industry, segment, owner, employee and revenue bands, a parent account for company groups, and a lifecycle_stage (target, engaged, opportunity, customer or churned).
A contact is a person at an account: a crm_contact row with its own first and last name, title, email, phone, owner and a lifecycle_stage that runs from subscriber, lead, mql (a marketing qualified lead, someone marketing judges ready for sales) and sql (a sales qualified lead, someone sales has accepted as worth pursuing) through opportunity and customer to evangelist or other. It also points at a core_person of type customer_contact. The shapes (account, contact, lead, opportunity, quote) follow the reference entities in the Microsoft Common Data Model [2], and the public vocabularies schema.org Organization [3] and schema.org Person [4] are what you use when you hand records to another system.
| Shape | What it is | Table | Status or stage values |
|---|---|---|---|
| Account | A company you sell to, or compete with | crm_account | lifecycle_stage: target, engaged, opportunity, customer, churned |
| Contact | A person at an account | crm_contact | lifecycle_stage: subscriber, lead, mql, sql, opportunity, customer, evangelist, other |
| Lead | An unqualified prospect, kept as it first arrived | crm_lead | status: new, working, nurturing, qualified, unqualified, converted |
| Opportunity | A deal that can be won or lost | crm_opportunity | a stage from the pipeline |
Leads and conversion
A lead comes from a web form or an import with a name, a company, an email, a phone, a source, an owner and the campaign tags in the page address (utm_source, utm_medium and so on) stored as utm. The rep records what was learned while qualifying as a JSON column named qualification. The specification names BANT (budget, authority, need, timing) and MEDDICC (metrics, economic buyer, decision criteria, decision process, identify the pain, champion, competition) as examples; use the fields your business actually asks.
Conversion is one database function that runs as a single transaction (all of its changes happen, or none do). It reuses the core_party if the company already exists or makes it; it reuses the crm_account if that party already has one (an existing customer stays a customer, and a target or engaged account becomes an opportunity account), and otherwise makes it, because the database allows one account per party; it makes a core_person and the contact, makes the opportunity at the first stage with an owner and a next step, marks the lead converted with the three new ids and the time, and writes a crm.lead.converted event. The lead row itself is kept.
Duplicates and consent
Two records for one customer are the most common CRM fault. Decide the rule before loading anything, write it in plain sentences and apply it the same way every time. A reasonable rule: a company is the same if its website address matches, or if its name matches after lowercasing, removing punctuation and dropping endings such as Inc, LLC and Ltd; a contact is the same if its email matches after lowercasing; a contact with no email is never merged by a script and goes to a person to review. Merging moves the dropped record's roles, activities and deals to the kept one and archives the dropped row. Nothing is deleted.
Contact data is personal data. Each contact carries do_not_contact, which is required and must be respected by every screen and route that sends a message, and the lead or contact keeps the wording of the consent shown, the time and the lawful basis relied on. The rules differ by place and by how you contact people: the United States CAN-SPAM Act for commercial email [5], the EU General Data Protection Regulation [6], and the California Consumer Privacy Act as amended by CPRA [7]. Which basis applies to your business is a decision for you and your adviser. The module records the answer you give; it does not invent one.
You need: Your current CRM or contact list, a text editor, and your AI coding agent if you want help with the table
Use real records from the tool you pay for today. Do not paste personal details into a chat; describe each record by its type and fields.
Outcome: A one-page note with three real records sorted into the module's shapes, a written duplicate rule and the consent you hold for each person.
Knowledge check
A company you already invoice is now also a sales prospect. What does the CRM add?
Knowledge check
A qualified lead is converted. What must be true afterwards?
References
- HubSpot developers: understanding the CRM. https://developers.hubspot.com/docs/api/crm/understanding-the-crm
- Microsoft Learn: Common Data Model. https://learn.microsoft.com/en-us/common-data-model/
- schema.org: Organization. https://schema.org/Organization
- schema.org: Person. https://schema.org/Person
- Federal Trade Commission: CAN-SPAM Act, a compliance guide for business. https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business
- EUR-Lex: Regulation (EU) 2016/679, the General Data Protection Regulation. https://eur-lex.europa.eu/eli/reg/2016/679/oj
- California Attorney General: California Consumer Privacy Act. https://oag.ca.gov/privacy/ccpa
Chapter 2 · Opportunities, tables and screens
Pipelines, stage history and the forecast
A deal is only as useful as its history. This chapter shows how stages are defined, why every change is kept, how the pipeline is rebuilt as of any past date, and how the same rows give you a forecast and the five measures a sales manager asks for. It ends with every column of the module's thirteen tables and the screens that use them.
35 minEvery change kept13 tables, every column5 screens
By the end of this chapter you can
- Define a pipeline and its stages with probability, won and lost flags and required fields.
- Explain why storing only the current stage makes velocity and win-rate analysis impossible.
- Rebuild the open pipeline as of a past date from crm_stage_history.
- Calculate win rate, cycle time and coverage from the module's own tables.
- Find every column, type, required flag and allowed value of the thirteen tables, and say who opens each screen.
Pipelines and stages
A crm_pipeline is a named sales process, for opportunities or for leads, and one of them is the default. A crm_pipeline_stage belongs to a pipeline and has a name, a position (seq), a probability, an is_won flag, an is_lost flag and required_fields: the columns that must be filled before a deal may enter the stage, such as amount, close_date or primary_contact_id. Setting probabilities from your own past results is better than copying a vendor's defaults, which describe someone else's business.
A crm_opportunity points at an account, a pipeline and a stage. It has a name, an amount and currency, an expected close_date, a probability that follows the stage, a forecast_category, an owner (required), a source and a next_step. When it is lost it also has a reason from the shared core_reason_code list and, if known, the competitor as a core_party. Two habits keep the pipeline honest: every open deal has an owner and a next step, and no deal is lost without a reason.
Who is on the deal
Real deals have several people. A crm_contact_role links a contact to an opportunity with one role. The seven roles are decision_maker, economic_buyer (holds the budget), champion (argues for you inside), influencer, technical_evaluator, user and blocker (opposes the deal). A deal with no economic buyer or decision maker on it is worth asking about before it is called likely.
The timeline
Calls, emails, meetings, tasks, notes, texts and site visits are crm_activity rows with a subject, a direction (inbound, outbound or internal), an owner, an outcome and a related record, so one list shows everything that happened on an account or a deal. The specification includes Gmail and Outlook sync and reminders; this course leaves sync for later, and keeps an external_id column so a later sync or an import never logs the same message twice.
Keep every stage change
The worst mistake in a home-made CRM is a stage column that is simply overwritten. It looks fine on the board and makes every serious question unanswerable: how long deals sit in each stage, where they are lost, what the pipeline looked like at the end of last quarter. The remedy is a crm_stage_history table that gets one row for every change, with the stage it came from, the stage it went to, when, who, and the amount at that moment.
Let the database write the history, not the screen. A trigger (a rule inside the database that runs by itself when a row changes) on crm_opportunity inserts the row on every insert and every change of stage, so a change made by hand in SQL leaves a row too. A move function adds the business rules: it refuses a stage from another pipeline, refuses the move if a required field is empty and names it, and refuses an open stage with no next step.
When a deal is won, the module sets its forecast_category to closed, makes the account a customer (a partner or distributor keeps its type) and publishes crm.opportunity.won. When it is lost, its forecast_category is also set to closed and a reason is required. Both outcomes are stages with is_won or is_lost set, so they appear in the history like any other move.
Forecast and the five measures
Every deal sits in one forecast category: pipeline, best_case, commit, closed or omitted. Roll-ups add the categories by owner, by territory (a crm_territory row has a name, JSON assignment rules and an owner) and by period. Coverage compares the open pipeline to the quota.
| KPI | Definition | Why it matters |
|---|---|---|
| Win rate | Won ÷ (won + lost), by count and by value | Shows how often you win once a deal is decided |
| Sales cycle length | Median days from created to closed won | Median, because a few very long deals would stretch an average |
| Pipeline coverage | Open pipeline in the period ÷ quota | Shows whether there is enough to hit the number |
| Quote turnaround | Hours from request to quote sent | The specification calls it key for job shops |
| Forecast accuracy | Commit compared with actual closed | Shows how far to trust the next forecast |
A worked example from the course seed: of six closed deals, four were won. By count the win rate is 4 ÷ 6, about 67%. By value, 13000 of 21000 was won, about 62%. The two differ because the lost deals were not the same size as the won ones. Always say which one you are quoting. The median days from first stage to won in the seed is 51.5.
Every table, column by column
Every table in the module starts with the standard columns every platform table has: id, tenant_id, created_at, created_by, updated_at, updated_by, row_version, archived_at and ext. The table below is the full definition of the thirteen tables this course builds: every column the module adds to the standard ones, with its type and whether it is required (marked req), and the allowed values where the specification lists them. Types are written as in the specification: text, int, num (a decimal number), bool, date, ts (a timestamp with time zone), uuid, json (jsonb in Postgres), money (an amount with fixed decimals, never floating point) and qty (a quantity that allows decimals); an arrow means a foreign key to that table. It is what your agent builds from in Session 2.
| Table | Holds | Columns: type, req = required |
|---|---|---|
| crm_account | The sales view of one company, one per core_party | party_id → core_party req, unique per tenant; account_type text req: prospect, customer, partner, competitor or distributor; industry text; segment text; owner_id → core_person; parent_account_id → crm_account; employee_band text; revenue_band text; lifecycle_stage text: target, engaged, opportunity, customer or churned |
| crm_contact | A person at an account, also a core_person of type customer_contact | person_id → core_person; account_id → crm_account; first_name text req; last_name text req; title text; email text; phone text; owner_id → core_person; lifecycle_stage text: subscriber, lead, mql, sql, opportunity, customer, evangelist or other; do_not_contact bool req; source text |
| crm_lead | An unqualified prospect, kept as it arrived | first_name text; last_name text; company_name text; email text; phone text; source text; status text req: new, working, nurturing, qualified, unqualified or converted; owner_id → core_person; score num; qualification json; utm json; converted_contact_id → crm_contact; converted_account_id → crm_account; converted_opportunity_id → crm_opportunity; converted_at ts |
| crm_pipeline | A named sales process | name text req; object_type text req: opportunity or lead; is_default bool |
| crm_pipeline_stage | A stage with its probability and exit rule | pipeline_id → crm_pipeline req; name text req; seq int req; probability num req; is_won bool req; is_lost bool req; required_fields json |
| crm_opportunity | A deal that can be won or lost | name text req; account_id → crm_account req; primary_contact_id → crm_contact; pipeline_id → crm_pipeline req; stage_id → crm_pipeline_stage req; amount money; currency text; close_date date; probability num; forecast_category text req: pipeline, best_case, commit, closed or omitted; owner_id → core_person req; source text; lost_reason_code_id → core_reason_code; competitor_party_id → core_party; next_step text |
| crm_stage_history | One row for every stage change; never updated or deleted | opportunity_id → crm_opportunity req; from_stage_id → crm_pipeline_stage; to_stage_id → crm_pipeline_stage req; changed_at ts req; changed_by → core_person; amount_at_change money |
| crm_contact_role | A person's part in the buying group of a deal | opportunity_id → crm_opportunity req; contact_id → crm_contact req; role text req: decision_maker, economic_buyer, champion, influencer, technical_evaluator, user or blocker |
| crm_activity | A call, email, meeting, task, note, text or visit on any record | activity_type text req: call, email, meeting, task, note, sms or site_visit; subject text req; body text; due_at ts; completed_at ts; owner_id → core_person; related_type text req; related_id uuid req, with no foreign key because it can point at any record; direction text: inbound, outbound or internal; outcome text; external_id text |
| crm_price_book | A price list | name text req; currency text req; segment text; active bool req |
| crm_price_book_entry | An item's price in a price book | price_book_id → crm_price_book req; item_id → core_item req; list_price money req; valid_from date; valid_to date |
| crm_quote | A quote, one row per version | quote_no text req, unique per tenant; version int req; opportunity_id → crm_opportunity; account_id → crm_account req; contact_id → crm_contact; status text req: draft, in_review, approved, sent, accepted, rejected, expired or superseded; valid_until date; currency text req; subtotal money; discount_total money; tax_total money; total money; terms text; approved_by → core_person; accepted_at ts; acceptance_ref text; pdf_attachment_id → core_attachment |
| crm_quote_line | A line of a quote | quote_id → crm_quote req; line_no int req; item_id → core_item; description text req; qty qty req; uom_id → core_uom; unit_price money req; discount_pct num; line_total money; configuration json; lead_time_days int |
Two tables in the specification are not built in this course: crm_cost_estimate (behind a quote line, explained in chapter 3) and crm_territory (name text req, rules json, owner_id → core_person). Add them later the same way.
The screens
Each screen is a row in the Role and Exposure Matrix with the roles that may open it, so the route guard and the menus follow from it. Simple one-table changes, such as claiming a lead or setting the discount threshold, are written straight to the table from the screen, under row-level security.
| Screen | Address | Who opens it | What it shows |
|---|---|---|---|
| Lead inbox | /crm/leads | Sales rep and sales manager | Leads, new first, with claim, status, qualification and a convert button |
| Pipeline board | /crm/pipeline | Sales rep and sales manager | One column for each stage (one stage at a time on a phone, with a stage picker); the manager also edits stages, probabilities and required fields |
| Quote builder | /crm/quotes | Sales rep and sales manager | Items from the price book with totals as the server computes them, status and versions; the manager also sees the approval queue and the discount threshold |
| Account lookup | /crm/accounts | Sales rep, sales manager and service staff | Accounts and contacts, with do_not_contact shown as a word; service staff see no deals, quotes or leads |
| Reports | /crm/reports | Sales manager | Pipeline as of a chosen date, win rate, cycle time and forecast by category; built in Session 6 |
Knowledge check
Why does the module store a crm_stage_history row for every stage change?
Knowledge check
Six deals are closed and four of them were won. What is the win rate by count?
References
- HubSpot developers: understanding the CRM. https://developers.hubspot.com/docs/api/crm/understanding-the-crm
- Metabase: shared sales CRM data model. https://www.metabase.com/integrations/sales-crm
- Syncari: HubSpot database schema compared with Salesforce. https://syncari.com/blog/hubspot-database-schema/
Chapter 3 · From price book to accepted order
Quotes, approvals and cost estimates
A quote is a promise with a price on it. This chapter shows how prices come from a price book on the server, how discounts get approved, how a changed quote becomes a new version, and how a shop that makes to order builds a price from a routing and a bill of materials.
25 minPrice on the server8 quote statusesCost ÷ (1 − margin)
By the end of this chapter you can
- Price a quote from a price book and check the stored totals against a hand calculation.
- Describe the quote statuses and the moves between them, including approval and versioning.
- Say what acceptance records and what event it publishes for the sales order.
- Build a unit price from cost and a target margin, and tell margin from markup.
Price books
A crm_price_book is a named price list with a currency, an optional segment (a customer group) and an active flag. Each crm_price_book_entry gives an item from the shared core_item list a list_price and, optionally, the dates it is valid from and to. Currencies are written as the three-letter codes in ISO 4217 [1]. Keeping prices in one book per currency or segment means a price change is one edit, and a customer-specific or distributor price is another book, not another spreadsheet.
Pricing is the server's job
A quote line holds an item, a description, a quantity, a unit of measure, a unit_price, an optional discount_pct, a line_total, any configuration as JSON and a lead_time_days. The browser may send only the item, the quantity and a discount percentage. The server reads the list price from the active entry valid today, copies it into the line as unit_price and works out every total. A price typed into a browser is never trusted, and because the quote keeps its own copy, a later price book change never alters a quote that was already sent.
| Quote field | What it holds |
|---|---|
| quote_no, version | A unique number from the shared number sequence, taken in the same transaction so two people never get one number; version starts at 1 |
| opportunity_id, account_id, contact_id | The deal, the customer (required) and who the quote is for |
| status, valid_until, currency | Where the quote is, the date it stops being open, and its currency |
| subtotal, discount_total, tax_total, total | Worked out by the server; tax stays empty until you have a tax rule. line_total = qty x unit_price less discount_pct; subtotal = sum of qty x unit_price before discounts; discount_total = sum of line discounts; total = subtotal - discount_total (+ tax_total), which equals the sum of the line_totals |
| approved_by, accepted_at, acceptance_ref | Who approved it, when the customer accepted and the reference of the reply or signed PDF |
| pdf_attachment_id | The stored PDF of the quote as sent |
Approval, versions and acceptance
A sales manager sets a discount threshold, a percentage that follows your own policy. If any line is above it, the quote cannot go straight to the customer: it moves from draft to in_review and creates an approval request. A manager approves or rejects with a comment, and the person who built the quote cannot approve it, which is the separation of duties auditors look for.
A changed quote is a new row, not an edit: version plus one, the number with -v2 on the end, the lines copied and the old row marked superseded. Sending stores a PDF of the quote and records its id. Acceptance is the customer saying yes, and the module records it: status accepted, the time from the server's clock and a reference to the customer's reply or signed PDF, and it publishes crm.quote.accepted. The event carries the quote number, the total, the currency and one entry for each line. When the order module is installed it turns the event into a sales order, and the module's definition of done requires the order's lines to match the quote.
Estimating a made-to-order price
A shop that makes parts to order does not have a list price for a part it has never made. It builds the price from cost. The crm_cost_estimate table sits behind a quote line and holds the material_cost from the bill of materials, labor_cost, machine_cost, overhead_cost, outside_cost for work sent out, the total_cost, the target_margin_pct and the assumptions as JSON. Its routing_ref is a soft link to the product definition from the manufacturing modules, so the estimate reuses the routing and the bill of materials instead of copying them.
Margin and markup are not the same. A margin is a share of the price; a markup is a share of the cost. For a cost of 80 and a target margin of 20%, the price is 80 ÷ (1 − 0.20) = 100, and the margin is 20 of 100. A markup of 20% on the same cost gives 96, which is a margin of only about 16.7%. These round numbers are for the arithmetic only; use your own rates and costs. Write the assumptions down with the estimate, so that whoever reads the quote later can tell what was assumed about scrap, setup and quantity.
Quote turnaround, the hours from request to quote sent, is the measure the specification calls key for job shops. An estimate built from the routing is how you shorten it without a spreadsheet per customer.
You need: One recent real quote from your business, a calculator, and your platform's quote screen or your AI coding agent
Use the numbers from a real quote. Do the arithmetic first, before the system does it.
Outcome: A hand calculation that equals the stored totals, a written approval threshold, a refused self-approval and a quote with two versions.
Knowledge check
A browser posts a unit price of 1.00 for an item whose price book entry is 25.00. What does the server store?
Knowledge check
A job costs 80 and the target margin is 20%. What is the price?
References
- ISO: ISO 4217, currency codes. https://www.iso.org/iso-4217-currency-codes.html
- Microsoft Learn: Common Data Model. https://learn.microsoft.com/en-us/common-data-model/
- Metabase: shared sales CRM data model. https://www.metabase.com/integrations/sales-crm
- HubSpot developers: understanding the CRM. https://developers.hubspot.com/docs/api/crm/understanding-the-crm
Chapter 4 · Roles, links and cutover
Access, events and moving in from your old CRM
The module only replaces your CRM when the right people can do their work in it, other modules hear what happens, and your old data comes across with its history intact. This chapter covers the roles, the events, the move and the cutover.
20 min4 roles and routes4 events outHistory in full
By the end of this chapter you can
- List the roles the module needs and what each may do, and why the rights must cover every later session.
- Name the events the module publishes and consumes.
- Load accounts, contacts, deals, activities and history from an old CRM so reports continue unbroken.
- Run old and new side by side, compare the numbers, and retire the subscription.
Who may do what
Row-level security (the database refusing rows a role should not see) is generated from the Role and Exposure Matrix, so a right that is missing from the matrix is refused later, in the middle of a session that needs it. Write every right the module needs, then walk each later step against the matrix before you build.
| Role or route | May do | May not |
|---|---|---|
| Lead intake route | Insert one crm_lead from the web form, with its utm data and consent | Read a lead back, or set an owner |
| Sales rep | Claim leads, qualify and convert, move stages, log activities, build quotes and send them | Approve any quote |
| Sales manager | Approve or reject quotes, set the discount threshold and the stages, reassign work, run the import, read the team's pipeline and the reports | Approve a quote they built |
| Service staff | Read accounts and contacts, and see the do_not_contact flag | Change deals, quotes or prices |
A customer has no login, so recording a customer's acceptance is done by a rep, or by a separate acceptance route with its own row in the matrix. Respecting do_not_contact is a rule in the database and on every screen and route that sends a message, not a courtesy.
Events in and out
Modules talk through events written to a table in the same transaction as the change (core_event_outbox), so an event cannot be lost if the change succeeds. The CRM publishes crm.lead.converted, crm.opportunity.stage_changed, crm.opportunity.won and crm.quote.accepted. It consumes cms.web_form.submitted, which becomes a lead, mkt.lead.mql from marketing, and svc.ticket.created, which feeds account health.
Meetings and contacts also travel in standard file formats: a contact as a vCard [1] and a meeting as an iCalendar event [2]. They matter when you sync with Gmail or Outlook, or send an invitation, because every mail and calendar program reads them.
Moving the data in
Salesforce and HubSpot both export accounts or companies, contacts, deals or opportunities, activities and stage history through an API or as CSV files. Find out which of the five your own plan gives you, and read the vendor's export terms on your own account page. Exports hold personal data: keep them in a private folder outside the repository, and never paste them into a chat. Try everything on a throwaway copy of the database first.
- Write the mapping. Company becomes a core_party and a crm_account; person becomes a core_person and a crm_contact; deal becomes an opportunity; each activity keeps the old id in external_id. Map every old user to an existing person, and every old stage and lost reason to yours, and stop on any that has no mapping.
- Carry opt-outs across. Anyone marked unsubscribed or do-not-contact in the old tool gets do_not_contact set to true. Copy consent only if the old tool holds a record of it.
- Write the duplicate rule first, as in chapter 1, and write skipped duplicates to a report with both source ids. The deals, contacts and activities that point at a skipped duplicate are loaded against the kept record, and an opt-out on a skipped copy is carried to the kept contact, so a person opted out on either copy stays do_not_contact.
- Keep the source of every record in ext.source_system and ext.source_id, with a unique index over them, so running the import twice skips what is already loaded. A stage change rarely has an id of its own, so a history row's source id is the old deal id, the stage and the time of the change joined together.
- Load history in full. If the old tool kept only a deal's current stage, load one starting row dated when the deal was created, mark it as having no history, and write down that analytics before the import date cannot be rebuilt.
- Reconcile. For each file write rows received, loaded, skipped as duplicates and refused, adding up to the file. Check that open pipeline at the export date equals the old tool's report.
Cut over and retire
Run the old tool and the new module side by side for at least a week. Compare open pipeline, win rate, cycle time and forecast, and explain every difference. When they agree, move the team over, cancel the subscription and record the saving: what the old tool cost a year, from its own bill. Costs are only what the specification and your own bills give; the module itself adds none beyond the platform you already run.
Deletion and anonymisation requests from the people in your data must also be handled without breaking the sales record. Under the GDPR [3] a person may ask for their personal data to be erased. The module supports this with a function the sales manager runs, crm_anonymise_contact, which replaces the name, email and phone of the contact, its core_person and any lead converted to it with the word Removed, sets do_not_contact and keeps the ids, the opportunities, the amounts and the stage history, so the pipeline and the win rate do not change. Free-text notes may still hold the person's details and are edited by hand. The audit log still holds them as well, because the audit trigger copied the old and new values of every change, names, emails and phones included, into the append-only core_audit_log, and so does any export or backup taken earlier; what to do about those is for the owner and their adviser to decide. What the law requires of your business in a given case is for you and your adviser to decide.
You need: Access to your old CRM's export page and its bill, and a text editor
Plan only; do not export anything yet. Look at what the tool offers and what you would map.
Outcome: A one-page migration plan with the export list, the stage and reason mapping, the duplicate rule, the reconciliation table and the cancellation date.
Knowledge check
Your old CRM exports deals with every stage change. How should you load them so reports carry on without a gap?
Knowledge check
A contact asks you to erase their personal data. What does the module keep?
References
- IETF RFC 6350: vCard Format Specification. https://www.rfc-editor.org/rfc/rfc6350
- IETF RFC 5545: Internet Calendaring and Scheduling Core Object Specification (iCalendar). https://www.rfc-editor.org/rfc/rfc5545
- EUR-Lex: Regulation (EU) 2016/679, the General Data Protection Regulation. https://eur-lex.europa.eu/eli/reg/2016/679/oj
- Syncari: HubSpot database schema compared with Salesforce. https://syncari.com/blog/hubspot-database-schema/
Chapter 5 · 12 questions · 80% passes
Final assessment
Twelve questions across the element. Score 80% (10 of 12) to pass. Your LMS records your score and each answer; you can review the chapters and try again.
15 min12 questions≈ 15 minutesRetake allowed
Your result
CivOps AI Academy
Customers and CRM: Accounts, Pipeline and Quotes on One Spine
Element M15 complete · Learner
Your LMS records this completion. For the CivOps Foundation certificate, finish the Foundation Course at https://civops.io/learn.