Skip to content
CRM case study

Project 07 · CRM architecture

The CRM I designed for a car dealership, and how I closed its gaps.

Three layers, each saying what it is: how their system worked, what I designed, and how I would build it today.

Plain words first, diagrams you can explore, two interactive tools, and the code behind the redesign.

Lead sources
7
Departments
6
Lead groups
4
Gaps closed
11

By Majed Alandajani · Updated 2 November 2026

On this page

Context

Leads were arriving from seven channels and leaking between six departments.

In 2024 I worked as a marketing analyst at a car dealership. Customers reached it through advertising, the website, social media, SMS, email, outdoor campaigns, events and the call center, and six departments needed those customers: telesales, test drives, aftersales, the call center, fleet sales and marketing.

Nothing connected them. Leads were copied by hand, forwarded by email, tracked in a spreadsheet, and marked lost after a few calls in one afternoon. Nobody could say which campaign sold a car. I mapped how it worked, then designed the CRM end to end. This page shows both, and then the part I care about most: where my own first design fell short, and how I would close every gap today.

Layer 1 · What I found

Two snapshots of how a lead moved before the redesign.

Dashed boxes were done by hand. Select any box to read what happened there. Tool names are generalised.

AdvertisingWebsiteSocial mediaCall centerMarketing teamAgencyIntake toolEmailDealer systemAftersalesFleetTelesales
Select any box to see what it does and what it connects to. Swipe sideways to see the whole map.

Email forwarding

Every department received its leads as emails. Nothing tracked whether an email was opened, called or forgotten.

  • Hand-offs: every lead passed through at least one person who copied it somewhere else.

  • No source: no campaign could be tied to a sale, so budgets were set by feeling.

  • No single customer: the same person could exist in the task tool, the spreadsheet and the dealer system as three different people.

  • Lost too early: three calls in one afternoon decided that a customer was gone.

  • Aftersales outside: service requests never entered the shared tool.

Layer 2 · The design

One intake, one router, six department lists, and nothing lost at the end.

Every channel carries a UTM link, so each lead knows where it came from. Every lead enters one intake, is checked against existing customers, and is routed to the list of the department that needs it. Each department works its list through the same status engine, and every ending feeds a data warehouse, with sales and aftersales feeding each other through up-selling and cross-selling.

every outcomeEventsSMSOutdoorWebsiteEmailSocial mediaCall centerIntakeforms + UTMCustomer checkRouterTelesalesTest driveAftersalesCall centerFleetMarketingUp-sell, cross-sellData warehouse
Select any box to see what it does and what it connects to. Swipe sideways to see the whole map.

Router

Each request goes to the list of the department that needs it, with each department’s own requirements.

Layer 2 · Department flows

How a lead moves inside each department.

Telesales listNew, assignedContactNext actionHotWarmColdLostSaleReason recordedFollow-up date
Select any box to see what it does and what it connects to. Swipe sideways to see the whole map.

Next action

Interested, semi-interested, not interested or no answer. Interested leads are classified by when they plan to buy.

These flows are my design as I drew it, with drawing mistakes corrected: a label copied between two departments, and a loop that had no exit. Nothing was added to them.

Honest review

Eleven gaps in my first design.

A design is only as good as its weakest rule. Reading my own diagrams again, these are the gaps a CRM would have hit in its first month.

  • 1. No data model. Lists and statuses, but no single customer record, so one person could still become three leads.

  • 2. Routing by keywords. The script needed every keyword at once, overwrote its output file with each lead, read its dictionary the wrong way round, and dropped any lead its rules could not place.

  • 3. Scoring that contradicted itself. Purchase probabilities with gaps between the bands (71 to 79% belonged nowhere), and timeframes that overlapped.

  • 4. No assignment rule. "Divide to the team" did not say how.

  • 5. No deadlines. No target for the first call, and three attempts without saying how far apart.

  • 6. Complaints could close unsolved. No escalation, and no customer confirmation.

  • 7. Drawing errors. A loop with no exit, a role label copied from another department, and a reminder sent before the booking was confirmed.

  • 8. Open departments. Marketing and fleet were left "under procedure", and parts and service questions had no flow.

  • 9. The dealer system was a box. Not an integration: nothing said what crosses between the CRM and it, or who owns what.

  • 10. No consent. Reminders and re-engagement by SMS and WhatsApp, without recording consent or offering an opt-out.

  • 11. Benefits without measures. Lower advertising cost and more sales were promised, with no baseline and no indicator to prove them.

Layer 3 · Redesign

One customer, many leads, and every touch recorded.

The redesign starts where the first design was thinnest: the data. A customer is one record, keyed by one normalised mobile number. Every request is a lead attached to that customer, every call or visit is an activity, and consent lives on the customer, per channel.

one to manyownsCustomerone mobile, one recordConsentper channelLeadsource, UTM, statusAgentdepartment, branchActivityevery call and visitBookingtest drive or serviceComplaintescalatesDealer systemevents both ways
Select any box to see what it does and what it connects to. Swipe sideways to see the whole map.

Customer

Identified by one normalised Saudi mobile number, so the same person arriving from three channels is still one customer.

// One customer, many leads, and every touch recorded as an activity, so
// response times and performance are measured, not guessed.
export type Department = 'telesales' | 'testdrive' | 'aftersales' | 'callcenter' | 'fleet' | 'marketing';
export type Source = 'website' | 'social' | 'sms' | 'email' | 'outdoor' | 'event' | 'callcenter';

export interface Customer {
  id: string;
  phone: string;               // +9665XXXXXXXX: the one key that detects duplicates
  name: string;
  consent: { marketingSms: boolean; marketingWhatsApp: boolean; at: string };
  optedOutAt?: string;
}

export interface Lead {
  id: string;
  customerId: string;
  department: Department;
  inquiryType: string;         // chosen in the form, never guessed when present
  source: Source;
  utm?: { campaign: string; medium: string; content?: string };
  status: 'new' | 'assigned' | 'contacting' | 'qualified' | 'won' | 'lost' | 'nurture';
  temperature?: 'hot' | 'warm' | 'cold' | 'lost';
  ownerId?: string;
  attempts: number;
  lostReason?: string;
  createdAt: string;
  firstContactAt?: string;     // speed to lead is measured from createdAt to here
}

export interface Activity {
  leadId: string;
  agentId: string;
  kind: 'call' | 'whatsapp' | 'sms' | 'visit' | 'test_drive' | 'note';
  outcome: 'answered' | 'no_answer' | 'interested' | 'not_interested' | 'booked';
  at: string;
}

Layer 3 · Redesign

The form decides where a lead goes. Keywords only suggest.

My first routing script read free text and looked for keywords. It is shown below as I wrote it. It needed every keyword of a department in one message, so a customer who only wrote "buy" went nowhere; it opened its output file in write mode, so each new lead erased the last; and its loop read the dictionary the wrong way round. The redesign asks the customer what they want in the form itself, and keeps keywords for free text only, in Arabic and English, with a person checking anything unclear.

Pick a request type and follow it

Telesales list

Assigned to the telesales agent with the fewest open leads.

First call within 15 minutes in working hours, then up to three attempts on three different days.

Before: forwarded by email, or copied from the task tool into a shared spreadsheet.

def transfer_text_based_on_keywords(input_file, keywords_to_files):
    # Read the contents of the original file
    with open(input_file, 'r', encoding='utf-8') as file:
        text = file.read()

    # Check each set of keywords and transfer the text to the corresponding file if the keywords are found
    for keywords, output_file in keywords_to_files.items():
        if all(keyword in text for keyword in keywords):
            with open(output_file, 'w', encoding='utf-8') as file:
                file.write(text)
            print(f"The text has been successfully transferred to {output_file}!")

# Define the files and the corresponding keywords
keywords_to_files = {
    'TeleSales.txt': ('buy', 'cost', 'value'),
    'AfterSales.txt': ('part', 'maintenance', 'inspection', 'periodic'),
    'Fleet.txt': ('company', 'institution'),
}

# Use the function to transfer texts
transfer_text_based_on_keywords('path/to/input_file.txt', keywords_to_files)

Layer 3 · Redesign

One rule, with no gaps and no overlaps.

My first design had two classifications that disagreed: purchase probabilities with gaps between them, and purchase timeframes that overlapped. Nobody can estimate a probability on a phone call, but a customer can say when they plan to buy. The redesign keeps the four groups and uses three questions an agent can actually ask. Try it.

Classify a lead

When does the customer plan to buy?

Budget or financing confirmed?

Reached within three attempts?

Hot

Call back within 2 days and offer a test drive. The same agent keeps the lead for 15 days, then it is reassessed.

// One rule, with no gaps and no overlaps: the purchase timeframe decides the
// temperature, a confirmed budget or financing separates hot from warm, and a
// lead nobody reached after three attempts is nurtured, not thrown away.
export function classifyLead(timeframe: Timeframe, budgetConfirmed: boolean, answered: boolean): Temperature {
  if (timeframe === 'no') return 'lost';
  if (!answered) return 'cold';
  if (timeframe === 'd30') return budgetConfirmed ? 'hot' : 'warm';
  if (timeframe === 'm3') return 'warm';
  return 'cold';
}

// Days until the next follow-up for each temperature.
export const FOLLOW_UP_DAYS: Record<Temperature, number | null> = {
  hot: 2,     // and the same agent keeps it for 15 days before it is reassessed
  warm: 30,
  cold: 90,
  lost: null, // the reason is recorded; only marketing may re-engage, with consent
};

Layer 3 · Redesign

A lead in the redesign, from the form to the dealer system.

Captured with its source and its consent

The form asks what the customer wants, records the UTM source, and asks for consent to marketing messages separately from service messages.

// A service message, such as a booking reminder, needs no marketing consent.
// Offers and re-engagement do, per channel, and an opt-out wins over everything.
export function canSend(c: Customer, purpose: 'service' | 'marketing', channel: 'sms' | 'whatsapp'): boolean {
  if (purpose === 'service') return true;
  if (c.optedOutAt) return false;
  return channel === 'sms' ? c.consent.marketingSms : c.consent.marketingWhatsApp;
}

01 / 08

Layer 3 · Redesign

Promises become indicators.

My first design promised lower advertising costs and more sales. The redesign defines how each promise is checked, from the activity and lead records, so a campaign or a team is judged on numbers.

  • Speed to lead: minutes from the form to the first call.

  • Contact rate: the share of leads actually reached.

  • Lead to test drive, and test drive to sale: where the funnel loses people.

  • Conversion by source and campaign: from the UTM on every lead, so budgets follow real sales.

  • No-show rate: for test drives and service visits.

  • Complaint resolution time and escalations: per branch.

  • Overdue follow-ups per agent: the fair way to evaluate performance.

-- What the design is judged by: how fast a lead is answered, how many are
-- reached, and which source really sells, so the budget follows sales.
SELECT source,
       COUNT(*) AS leads,
       AVG(CASE WHEN first_contact_at IS NOT NULL THEN 1.0 ELSE 0 END) AS contact_rate,
       AVG((julianday(first_contact_at) - julianday(created_at)) * 1440) AS minutes_to_first_contact,
       AVG(CASE WHEN status = 'won' THEN 1.0 ELSE 0 END) AS conversion
FROM leads
WHERE created_at >= date('now', '-30 days')
GROUP BY source
ORDER BY conversion DESC;

In plain terms

What I designed, and what is redesign.

  • I designed this CRM in full while working as a marketing analyst. This page does not claim that the dealership runs it.

  • Their system is shown as I mapped it then, with the names of their tools generalised.

  • My design is shown as I drew it, with drawing mistakes corrected and nothing added.

  • The redesign is how I would build it today. Its code is reference code written for this case study, to show the engineering; the first routing script is the original from my design.

Why I show the gaps too

A design that looks perfect on a slide usually breaks on its first busy day. Reading my own work again, finding where it would have failed, and closing each gap is the part of systems work I trust most.

If your sales or service team is losing customers between tools, I am happy to talk.