Unified Contact Directory Architecture
How TotalApp models customers, vendors, leads, and companies as one record — and how CRM, Sales, Purchasing, and Call Center all read and write it.
Overview
Most CRM/ERP products end up with three or four overlapping "who is this?" concepts — a CRM Account, a Sales Customer, a Purchasing Vendor, a Contact — each stored separately, each drifting out of sync with the others. TotalApp avoids that by storing every person and company in a single unified Contact record. What a contact is — a customer, a vendor, a lead, a company, a manufacturer — is not which table it lives in, it's a set of boolean role flags on that one record. This page is the architectural reference for that model: the data shape, the lifecycle, and exactly how each module — CRM Accounts, Sales Pipeline, Vendor Management, and Call Center — reads and writes it.
Where "Accounts" Went — and Why It Came Back Differently
Earlier versions of TotalApp had a CRM screen called "Accounts," which clashed with Finance's Chart of Accounts (the same word meaning two different things depending on which module you were in — a well-known ambiguity in Odoo/Salesforce/HubSpot-style products). That screen became Customers, and "Accounts" was reserved for Finance's ledger accounts. TotalApp later added a company-centric CRM Accounts screen — but rather than backing it with a separate account table (the classic source of the original ambiguity), it reads and writes the exact same unified Contact directory described on this page, filtered to isCompany. There is still only one place a company record lives.
The Contact Record
Every person or company — regardless of what module you found them in — is one row with this shape:
| Field | Type | Purpose |
|---|---|---|
| id, name | string | Identity. |
| parentId | string | null | Self-referencing link to another Contact's id. On an individual, points at the company they work at (parentId = companyContact.id); on a company, always null. Listing "everyone at company X" is contacts.filter(c => c.parentId === companyId) — no separate join table. |
| company, email, phone | string | Standard contact details. |
| roles | object of booleans | Which relationships this contact has with you — see the Role Flags table below. Several can be true at once. |
| lifecycleStage | enum | suspect → lead → opportunity → customer. The contact's own CRM funnel position — distinct from any Deal's pipeline stage. |
| status | enum | active / inactive / pending. |
| tags | string[] | Free-text category tags (industry, vendor category, source like "call-center"). |
| industry, website, address | string (optional) | Company-profile fields. Present on every contact record, but only meaningful — and only editable through the UI — when roles.isCompany is true; individual contacts leave them blank. Shown and edited from CRM Accounts. |
| vendorProfile | object (optional) | Present only when roles.isVendor is true — tax ID, VAT number, address/city/country, payment terms, currency, categories, and performance metrics. See "Vendor Profile Fields" below. |
Role Flags
A contact's role flags can combine freely. A supplier who also buys your software is isVendor: true AND isCustomer: true on the exact same record — never two separate rows to keep in sync.
| Flag | Meaning | Read by |
|---|---|---|
| isCompany | Legal entity rather than an individual person. | Customers → Companies tab, CRM Accounts (entire screen) |
| isCustomer | Buys goods/services from you. | Customers → Customers filter |
| isVendor | Sells goods/services to you (a.k.a. supplier). | Vendor Management, Customers → Vendors filter |
| isManufacturer | Produces parts or goods (Manufacturing / MOM context). | Reserved for manufacturing-side screens. |
| isTransporter | Carrier / logistics provider. | Reserved for Warehouse/Logistics screens. |
| isLead | Sales prospect, not yet converted to a paying customer. | Customers → Leads filter, Sales Pipeline |
| isEmployee | Internal staff member. | Reserved for internal task/expense contexts. |
Filtering By Role, Not By Table
Because there is only one table, every screen that shows "customers" or "vendors" is really the same directory with a different filter applied. The CRM Accounts screen's role filter bar (All / Customers / Vendors / Companies / Leads) is the most visible example of this — switching filters never re-queries a different dataset, it just changes which flag is checked.
Lifecycle Stage vs. Deal Stage — Don't Confuse Them
This is the single most common point of confusion when integrating a new module against the contact directory, so it gets its own section:
- Contact.lifecycleStage belongs to the contact itself — one value, always up to date, describing where this person/company sits in your overall relationship funnel.
- Deal.stage (Qualified / Proposal / Negotiation / Closed Won / Closed Lost, in Sales Pipeline) belongs to a specific opportunity tied to that contact. A single contact can have several open Deals at once, each in a different stage, while the contact's own lifecycleStage stays wherever it currently is.
- The two are connected by exactly one automation: dragging a linked Deal to Closed Won advances the contact's lifecycleStage to
customer(see "Closed-Won Automation" below). No other Deal stage change touches lifecycleStage.
Module Integration Map
Five modules currently read from and write to the unified directory. Each uses a different entry point appropriate to how that module works day to day:
CRM Accounts
The directory's primary UI. Lists, role-filters, creates (isCustomer), and edits contacts directly. The Companies tab is the same data filtered to isCompany.
CRM Accounts
The company-only view of the same directory (isCompany). Creates and edits industry/website/address fields, and is the source for the Account picker on Deals and Activities.
Sales Pipeline
Links Deals to a person (contactId) and/or a company (accountId) — both point into the same directory. Automatically promotes whichever is linked to Customer when the Deal reaches Closed Won.
Vendor Management
Creates and edits contacts with isCompany + isVendor, attaching a vendorProfile (tax, payment terms, performance metrics) to the shared record.
Call Center
Every outbound call resolves the dialled number against the directory — matches an existing contact, or auto-creates a new Lead (isLead, lifecycleStage: suspect) so no caller is ever lost.
Deal.accountId and CrmActivity.accountId Point Into the Same Table
A Deal or an Activity carries both a contactId (a person row's id) and an optional accountId (a company row's id) — both fields resolve against the exact same Contact directory, just different rows. There's no separate accounts table for accountId to reference.
Automation Events
Two automations keep the directory current without manual re-entry. Both are one-way and idempotent — running them twice on an already-converted contact is a no-op.
| Trigger | Where | Effect on the Contact |
|---|---|---|
| Closed-Won conversion | A Deal linked to a contact and/or a company is dragged to a Closed Won column in Sales Pipeline. | roles.isCustomer → true, roles.isLead → false, lifecycleStage → customer — applied to contactId's row and, if different, accountId's row too. Deals with neither linked, or dropped into any non-won stage, do nothing. |
| Call Center auto-lead | An outbound call is started in Virtual PBX → Start Call to a phone number not already in the directory. | A new contact is created: roles.isLead → true, lifecycleStage → suspect, tags includes "call-center". Dialling the same number again reuses the existing contact instead of duplicating it. |
Automations Never Downgrade a Contact
Neither automation ever clears isCustomer or reverts lifecycleStage backwards. A contact only ever moves forward through the funnel automatically; demoting a contact (e.g. marking a former customer inactive) is always a manual edit.
Vendor Profile Fields
Reserved for contacts with roles.isVendor: true — folded onto the same record rather than a separate vendor table, so a contact that's both a customer and a vendor stays one row:
| Field | Description |
|---|---|
| taxId, vatNumber | Tax and VAT registration numbers. |
| address, city, country | Registered business location. |
| paymentTermsDays, currency | Payment terms and billing currency used across purchase documents. |
| categories | Free-text tags describing what the vendor supplies. |
| metrics | totalOrders, onTimeDeliveries, lateDeliveries, averageLeadTimeDays, qualityScore, reliabilityScore, totalSpent, lastOrderDate — computed from purchase order history and shown on the vendor's profile panel. |