Get Hedhog updates in your inbox
New releases, fresh recipes, and breaking changes — no spam.
CRM Submodule
persons
Manages people and companies in the CRM — contacts, accounts, interactions, follow-ups, activities, lifecycle stages and CSV import.
Source path: libraries/crm/src/person
Module file: person.module.ts
Introduction
The Persons submodule is the core of the CRM: a single `person` table represents both individuals and companies (discriminated by the `type` field), with company records additionally exposed through a dedicated Accounts surface (`/person/accounts`) that filters to `type: company` and adds account-specific fields such as industry, website, annual revenue and employee count. The base `person` table holds only structural fields (name, type, status, avatar). Everything else that PersonController accepts on create/update — notes, employer_company_id, owner_user_id, source, lifecycle_stage, next_action_at, score, deal_value, tags — is persisted as key/value rows in a companion person_metadata table and reassembled into the response shape by the service. This keeps the core schema stable while letting CRM-specific attributes evolve without a migration for every new field.
Ownership and lifecycle tracking are first-class concerns. Every person can be assigned an owner_user_id (gated by the admin, admin-crm, or owner-crm role) and a lifecycle_stage drawn from a sales funnel (new, contacted, qualified, proposal, negotiation, customer, lost). POST /person/:id/lifecycle-stage records the transition explicitly rather than silently overwriting the value on a generic update, which keeps a clean history for reporting. GET /person/dashboard and GET /person/reports aggregate these stage transitions and interaction logs into funnel metrics and time-bucketed reports (day/week/month/year), giving the admin panel its pipeline visualizations without separate data warehousing.
Interactions (call, email, whatsapp, meeting, note) and follow-ups (a scheduled next_action_at plus notes) are logged per person and also surfaced as cross-person worklist endpoints — GET /person/followups and GET /person/activities return paginated, filterable views across the whole CRM so a rep can work a queue without opening every person record individually. Activities are a separate, more structured concept than interactions: they carry status and can be explicitly completed via POST /person/activities/:id/complete.
Duplicate prevention and consolidation are handled by two complementary endpoints. GET /person/duplicates performs a fuzzy check by name, email or phone before a new record is created. POST /person/merge consolidates an existing duplicate pair: contacts, documents, addresses and metadata are merged into the target record inside a single transaction, the source person is marked inactive, and a merged_into_person_id metadata pointer is left behind for traceability. Only persons of the same type can be merged, and only the contact_only strategy is currently implemented — the DTO's strategy field is typed for future strategies but the service only accepts this one value today.
Bulk onboarding is supported via a two-step CSV flow: POST /person/import/preview parses the uploaded file and returns headers and sample rows so the caller can build a column mapping, then POST /person/import applies that mapping (passed as a JSON string in a multipart field) to create person records, optionally associating them all with a company via company_id. Both endpoints reject non-CSV uploads and cap file size at 10 MB.
GET /person/avatar/:id is the one public, unauthenticated endpoint in the submodule — it serves a person's avatar image by file ID with ETag-based caching (If-None-Match) so the admin panel and any embedded widgets can render avatars without needing a session token. Linking a person to a platform user account (user_id) is restricted to a dedicated person-user-linker role in addition to admin/admin-crm, since it grants that user visibility tied to the CRM record.
HTTP Endpoints
31 endpoints
Get global CRM person statistics (totals by type and status). When company registration is disabled, company counts are reported as zero.
Get CRM dashboard data (funnel/lifecycle metrics) for a given period, optionally scoped to an owner.
- Query
- owner_user_id?, period? (7d|30d|90d|custom), date_from?, date_to?
Get CRM reports grouped by time period.
- Query
- date_from (required), date_to (required), group_by (day|week|month|year, required)
List user options to assign as person owners (autocomplete; restricted to admin, admin-crm, or owner-crm role holders).
- Query
- search?
List platform user options that can be linked to a person record.
- Query
- search?
Check whether a user is already linked to any person (or to a specific one).
- Query
- userId (required), personId?
Check for potential duplicate persons based on name, email, or phone before creating a new record.
- Query
- name?, email?, phone?
Paginated list of persons with optional filters.
- Query
- page, pageSize, search, type?, status?, lifecycle_stage?, owner_user_id?, source?, tags?
Serve a person's public avatar image (no auth required), with ETag-based caching via If-None-Match.
- Params
- id (int) — file ID of the avatar
Get full details for a single person including contacts, addresses, documents, and relationships.
- Params
- id (int)
Create a new person (individual or company).
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | — |
| type | PersonType (individual | company) | yes | — |
| status | PersonStatus (active | inactive) | yes | Defaults to active |
| avatar_id | int | no | — |
| birth_date | date string | no | — |
| gender | PersonGender (male | female | other) | no | — |
| job_title | string | no | — |
| trade_name | string | no | Company trade/fantasy name |
| foundation_date | date string | no | — |
| legal_nature | string | no | — |
| notes | string | no | Stored as person_metadata |
| employer_company_id | int | no | Stored as person_metadata |
| owner_user_id | int | no | Stored as person_metadata |
| user_id | int | no | Link to a platform user |
| source | PersonSource (referral | website | social | inbound | outbound | other) | no | Stored as person_metadata |
| lifecycle_stage | PersonLifecycleStage (new | contacted | qualified | proposal | negotiation | customer | lost) | no | Stored as person_metadata |
| next_action_at | date string | no | Stored as person_metadata |
| score | number | no | Stored as person_metadata |
| deal_value | number | no | Stored as person_metadata |
| tags | string[] | no | — |
Merge two duplicate persons of the same type, consolidating contacts/documents/addresses/metadata into the target inside a single transaction.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| source_person_id | int | yes | Person to be merged and marked inactive |
| target_person_id | int | yes | Person to keep |
| strategy | "contact_only" | yes | Only contact_only is currently implemented |
Preview CSV import — returns column headers and sample rows for field mapping.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| file | multipart/CSV (max 10 MB) | yes | CSV file to preview before import |
Import persons from a CSV file with custom field mapping.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| file | multipart/CSV (max 10 MB) | yes | — |
| mapping | JSON string (Record<string, string>) | yes | CSV column → person field mapping |
| company_id | int (form field) | no | Associate imported persons with a company |
Update person data (partial update, same fields as create — contacts/addresses/documents arrays are synchronized: created, updated or removed as needed).
- Params
- id (int)
Delete one or more persons (cascades to contacts, documents, addresses, relations and metadata).
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| ids | number[] | yes | IDs to delete |
List all interactions logged for a person.
- Params
- id (int)
Log a new interaction for the person.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| type | PersonInteractionType (call | email | whatsapp | meeting | note) | yes | — |
| notes | string | no | — |
Paginated list of follow-ups across all persons, filterable by status or date range.
- Query
- page, pageSize, search, status? (all | today | upcoming | overdue), date_from?, date_to?
Summary statistics for follow-ups (counts by status).
- Query
- search?
Schedule a follow-up action for a person.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| next_action_at | date string | yes | — |
| notes | string | no | — |
Update the CRM lifecycle stage for a person, explicitly recording the stage transition for funnel reporting.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| lifecycle_stage | PersonLifecycleStage (new | contacted | qualified | proposal | negotiation | customer | lost) | yes | — |
Paginated list of activities across all persons.
- Query
- page, pageSize, search, status?, type?, person_id?
Summary statistics for all activities (counts by status/type).
Get full detail for a single activity.
- Params
- id (int)
Mark an activity as completed.
- Params
- id (int)
Paginated list of company accounts (persons of type company) with CRM lifecycle filtering.
- Query
- page, pageSize, search, status? (all | active | inactive), lifecycle_stage?, sortField? (name | created_at), sortOrder? (asc | desc)
Summary statistics for company accounts (counts by lifecycle stage).
Create a new company account.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | — |
| status | PersonStatus (active | inactive) | yes | — |
| trade_name | string | no | — |
| foundation_date | date string | no | — |
| legal_nature | string | no | — |
| industry | string | no | — |
| website | string | no | — |
| email string | no | — | |
| phone | string | no | — |
| owner_user_id | int | no | — |
| annual_revenue | number | no | — |
| employee_count | int | no | — |
| lifecycle_stage | AccountLifecycleStage (prospect | customer | churned | inactive) | no | — |
| city | string | no | — |
| state | string (max 2) | no | — |
| collaborator_person_ids | int[] | no | — |
| contacts | UpdateAllContactDTO[] | no | — |
| addresses | UpdateAllAddressDTO[] | no | — |
| documents | UpdateAllDocumentDTO[] | no | — |
| avatar_id | int | no | — |
Update a company account (same fields as create).
- Params
- id (int)
Delete one or more company accounts.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| ids | number[] | yes | IDs to delete |
MCP Tools
Named tools callable by AI models via the Model Context Protocol (MCP).
crm.persons.listLists persons (individuals or companies) with pagination and filters
crm.persons.statsReturns person statistics: total, individual, company, active, inactive counts
crm.persons.dashboardReturns CRM dashboard data with funnel metrics, grouped by period
crm.persons.reportsReturns CRM reports data
crm.persons.owner-optionsReturns users available as owners for a person record
crm.persons.linked-user-optionsReturns users that can be linked to a person record
crm.persons.duplicatesChecks for duplicate person records by name, email, or phone
crm.persons.getReturns a single person record by ID with full details
crm.persons.createCreates a new person record (individual or company)
crm.persons.updateUpdates an existing person record by ID
crm.persons.deleteDeletes one or more person records by their IDs
crm.persons.mergeMerges two duplicate person records into one
crm.persons.interactions.listLists all interactions for a person (calls, emails, meetings, notes)
crm.persons.interactions.createRecords a new interaction with a person
crm.persons.followups.listLists all follow-ups across all persons with pagination and filters
crm.persons.followups.statsReturns follow-up statistics grouped by status and period
crm.persons.followups.createSchedules a follow-up for a person
crm.persons.activities.listLists CRM activities with pagination and filters
crm.persons.activities.statsReturns activity statistics
crm.persons.activities.getReturns a single CRM activity by ID
crm.persons.activities.completeMarks a CRM activity as completed
crm.persons.lifecycle-stage.updateUpdates the lifecycle stage of a person record
crm.persons.accounts.listLists company accounts with pagination and filters
crm.persons.accounts.statsReturns account statistics
crm.persons.accounts.createCreates a new company account
crm.persons.accounts.updateUpdates an existing company account by ID
crm.persons.accounts.deleteDeletes one or more company accounts by their IDs