getodden / crm-core
Headless CRM Core Engine for Odden (Contacts, Companies, Custom Properties, Associations, Timeline)
v0.4.1
2026-10-02 22:48 UTC
Requires
- php: ^8.3
- laravel/framework: ^12.0 || ^13.0
Requires (Dev)
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^3.8 || ^4.0 || ^5.0
- pestphp/pest-plugin-laravel: ^3.0 || ^4.0 || ^5.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
This is a read-only split of the getodden/crm monorepo. Please open issues and pull requests there.
The headless CRM foundation engine for the Odden RevOps platform. Manages contacts, companies, extensible custom properties (EAV), polymorphic associations, activity audit timelines, list segmentation, and lifecycle stage state transitions.
Architecture & Capabilities
+-------------------------------------------------------------------------+
| ODDEN CORE |
| |
| +--------------------+ +--------------------+ +-----------------+ |
| | Contacts | | Companies | | Custom Objects | |
| +--------------------+ +--------------------+ +-----------------+ |
| \ / / |
| v v v |
| +---------------------------------------------------------+ |
| | Polymorphic Association Engine | |
| | (1:1, 1:N, M:N with directional cardinalities) | |
| +---------------------------------------------------------+ |
| | | | |
| v v v |
| +--------------------+ +--------------------+ +-----------------+ |
| | Custom Properties | | Activity Timelines | | Lifecycle State | |
| | (EAV + Audit Log) | | (Calls, Meetings) | | Machine & Health| |
| +--------------------+ +--------------------+ +-----------------+ |
+-------------------------------------------------------------------------+
Core Features
- Extensible EAV Properties: Dynamically define and validate custom properties (
text,number,date,datetime,boolean,select,multiselect,json) across any CRM record, complete with full historical audit trails (PropertyHistory). - Polymorphic Association Engine: Connect any record to any other record (
Contact,Company,Deal,Ticket, or custom object) with directional types and enforced cardinality (OneToOne,OneToMany,ManyToMany). - Unified Activity Timeline: Log calls, notes, emails, meetings, tasks, WhatsApp, SMS, and LinkedIn messages with polymorphic subject attachment.
- Strict Lifecycle State Machine: Enforce deterministic progression (
Subscriber->Lead->MQL->SQL->Opportunity->Customer->Evangelist), with full transition tracking and velocity metrics. - Automated Association & Domain Extraction: Automatically match inbound contacts to companies using corporate email domains while filtering out 4,000+ public freemail providers (
gmail.com,yahoo.com, etc.). - Deduplication & Record Merging: Identify contact/company duplicates via exact or fuzzy matchers, and safely merge secondary records into a primary survivor while repointing associations, activities, and property histories.
- Dynamic & Static Segmentation: Filter and maintain active CRM audience lists (
CrmList) using declarative condition criteria. - Customer Health Scoring: Calculate multidimensional health scores (
Healthy,At Risk,Critical) derived from activity recency, open support tickets, and deal interactions.
Installation
composer require getodden/crm-core
Publish migrations and configuration (optional):
php artisan vendor:publish --tag=odden-core-migrations php artisan vendor:publish --tag=odden-core-config
Run migrations:
php artisan migrate
Quick Start & Code Examples
1. Creating Contacts & Automatic Company Association
use Odden\Core\Actions\CreateContactAction; use Odden\Core\Enums\LifecycleStage; $contact = app(CreateContactAction::class)->execute([ 'first_name' => 'Jane', 'last_name' => 'Doe', 'email' => 'jane@acme.corp', 'lifecycle_stage' => LifecycleStage::Lead, 'custom_properties' => [ 'job_title' => 'VP of Engineering', 'annual_cloud_budget' => 150000, ], ]); // If Acme Corp exists with domain "acme.corp", Jane is automatically associated.
2. Polymorphic Associations
use Odden\Core\Actions\AssociateRecordsAction; use Odden\Core\Models\AssociationType; use Odden\Core\Enums\AssociationCardinality; $type = AssociationType::firstOrCreate([ 'name' => 'billing_contact', 'label' => 'Billing Contact', 'cardinality' => AssociationCardinality::OneToOne, ]); app(AssociateRecordsAction::class)->execute( from: $company, to: $contact, type: $type );
3. Lifecycle Stage State Transitions
use Odden\Core\Actions\TransitionLifecycleStageAction; use Odden\Core\Enums\LifecycleStage; // Transitions validate against allowed paths and record a LifecycleStageTransition audit entry app(TransitionLifecycleStageAction::class)->execute( record: $contact, newStage: LifecycleStage::Mql, actorId: auth()->id() );
4. Logging Timeline Activities
use Odden\Core\Actions\LogActivityAction; use Odden\Core\Enums\ActivityType; app(LogActivityAction::class)->execute([ 'type' => ActivityType::Call, 'subject' => 'Q4 Contract Review', 'body' => 'Client agreed to proceed with annual enterprise tier.', 'subjectable' => $contact, 'author_id' => auth()->id(), 'metadata' => [ 'call_duration_seconds' => 940, 'recording_url' => 'https://storage.acme.corp/calls/rec_9281.mp3', ], ]);
5. Deduplication & Record Merging
use Odden\Core\Actions\FindDuplicateContactsAction; use Odden\Core\Actions\MergeContactsAction; // Discover duplicates by email or normalized name $duplicates = app(FindDuplicateContactsAction::class)->execute($contact); if ($duplicates->isNotEmpty()) { // Merges secondary into primary, re-parenting associations, logs, and properties app(MergeContactsAction::class)->execute( primary: $contact, secondary: $duplicates->first(), actorId: auth()->id() ); }
Data Models & Schema Reference
| Model | Table | Responsibility |
|---|---|---|
Contact |
contacts |
Individual persons, emails, phones, lifecycle stages, and custom properties. |
Company |
companies |
Organizations, verified corporate domains, employee sizes, and health scores. |
PropertyDefinition |
property_definitions |
Dynamic property registry specifying data types, validation rules, and target entities. |
PropertyHistory |
property_histories |
Point-in-time EAV change logs with actor attribution. |
Association |
associations |
Polymorphic graph connecting any two CRM records. |
AssociationType |
association_types |
Relationship taxonomy defining cardinality and directional semantics. |
Activity |
activities |
Time-stamped touchpoints (calls, meetings, notes, tasks) attached to entities. |
CrmList |
crm_lists |
Static or dynamic segmentation rules and filter definitions. |
LifecycleStageTransition |
lifecycle_stage_transitions |
Immutable history of record lifecycle stage movements. |
CustomObjectDefinition |
custom_object_definitions |
Schema definitions for custom user-created entities. |
CustomObjectRecord |
custom_object_records |
Concrete instances of user-defined entities. |
Testing
vendor/bin/pest packages/core/tests --compact